Merge branch 'vector-im/develop' into unifiedpush

This commit is contained in:
user 2021-06-02 08:15:42 +02:00
commit 88cfae9e09
113 changed files with 1572 additions and 505 deletions

View File

@ -51,12 +51,12 @@ data class SsoIdentityProvider(
) : Parcelable, Comparable<SsoIdentityProvider> { ) : Parcelable, Comparable<SsoIdentityProvider> {
companion object { companion object {
const val BRAND_GOOGLE = "org.matrix.google" const val BRAND_GOOGLE = "google"
const val BRAND_GITHUB = "org.matrix.github" const val BRAND_GITHUB = "github"
const val BRAND_APPLE = "org.matrix.apple" const val BRAND_APPLE = "apple"
const val BRAND_FACEBOOK = "org.matrix.facebook" const val BRAND_FACEBOOK = "facebook"
const val BRAND_TWITTER = "org.matrix.twitter" const val BRAND_TWITTER = "twitter"
const val BRAND_GITLAB = "org.matrix.gitlab" const val BRAND_GITLAB = "gitlab"
} }
override fun compareTo(other: SsoIdentityProvider): Int { override fun compareTo(other: SsoIdentityProvider): Int {

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2021 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.call
import java.util.UUID
object CallIdGenerator {
fun generate() = UUID.randomUUID().toString()
}

View File

@ -26,8 +26,12 @@ interface MxCallDetail {
val callId: String val callId: String
val isOutgoing: Boolean val isOutgoing: Boolean
val roomId: String val roomId: String
val opponentUserId: String
val isVideoCall: Boolean val isVideoCall: Boolean
val ourPartyId: String
val opponentPartyId: Optional<String>?
val opponentVersion: Int
val opponentUserId: String
val capabilities: CallCapabilities?
} }
/** /**
@ -39,12 +43,6 @@ interface MxCall : MxCallDetail {
const val VOIP_PROTO_VERSION = 1 const val VOIP_PROTO_VERSION = 1
} }
val ourPartyId: String
var opponentPartyId: Optional<String>?
var opponentVersion: Int
var capabilities: CallCapabilities?
var state: CallState var state: CallState
/** /**
@ -91,8 +89,12 @@ interface MxCall : MxCallDetail {
/** /**
* Send a m.call.replaces event to initiate call transfer. * Send a m.call.replaces event to initiate call transfer.
* See [org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent] for documentation about the parameters
*/ */
suspend fun transfer(targetUserId: String, targetRoomId: String?) suspend fun transfer(targetUserId: String,
targetRoomId: String?,
createCallId: String?,
awaitCallId: String?)
fun addListener(listener: StateListener) fun addListener(listener: StateListener)
fun removeListener(listener: StateListener) fun removeListener(listener: StateListener)

View File

@ -54,7 +54,7 @@ interface PermalinkService {
* *
* @return the permalink, or null in case of error * @return the permalink, or null in case of error
*/ */
fun createRoomPermalink(roomId: String): String? fun createRoomPermalink(roomId: String, viaServers: List<String>? = null): String?
/** /**
* Creates a permalink for an event. If you have an event you can use [createPermalink] * Creates a permalink for an event. If you have an event you can use [createPermalink]

View File

@ -32,5 +32,7 @@ data class SpaceChildInfo(
val parentRoomId: String?, val parentRoomId: String?,
val suggested: Boolean?, val suggested: Boolean?,
val canonicalAlias: String?, val canonicalAlias: String?,
val aliases: List<String>? val aliases: List<String>?,
val worldReadable: Boolean
) )

View File

@ -56,6 +56,9 @@ data class CallHangupContent(
@Json(name = "user_hangup") @Json(name = "user_hangup")
USER_HANGUP, USER_HANGUP,
@Json(name = "replaced")
REPLACED,
@Json(name = "user_media_failed") @Json(name = "user_media_failed")
USER_MEDIA_FAILED, USER_MEDIA_FAILED,

View File

@ -38,23 +38,23 @@ data class CallReplacesContent(
*/ */
@Json(name = "replacement_id") val replacementId: String? = null, @Json(name = "replacement_id") val replacementId: String? = null,
/** /**
* Optional. If specified, the transferee client waits for an invite to this room and joins it * Optional. If specified, the transferee client waits for an invite to this room and joins it
* (possibly waiting for user confirmation) and then continues the transfer in this room. * (possibly waiting for user confirmation) and then continues the transfer in this room.
* If absent, the transferee contacts the Matrix User ID given in the target_user field in a room of its choosing. * If absent, the transferee contacts the Matrix User ID given in the target_user field in a room of its choosing.
*/ */
@Json(name = "target_room") val targerRoomId: String? = null, @Json(name = "target_room") val targetRoomId: String? = null,
/** /**
* An object giving information about the transfer target * An object giving information about the transfer target
*/ */
@Json(name = "target_user") val targetUser: TargetUser? = null, @Json(name = "target_user") val targetUser: TargetUser? = null,
/** /**
* If specified, gives the call ID for the transferee's client to use when placing the replacement call. * If specified, gives the call ID for the transferee's client to use when placing the replacement call.
* Mutually exclusive with await_call * Mutually exclusive with await_call
*/ */
@Json(name = "create_call") val createCall: String? = null, @Json(name = "create_call") val createCall: String? = null,
/** /**
* If specified, gives the call ID that the transferee's client should wait for. * If specified, gives the call ID that the transferee's client should wait for.
* Mutually exclusive with create_call. * Mutually exclusive with create_call.
*/ */
@Json(name = "await_call") val awaitCall: String? = null, @Json(name = "await_call") val awaitCall: String? = null,
/** /**
@ -77,6 +77,5 @@ data class CallReplacesContent(
* Optional. The avatar URL of the transfer target. * Optional. The avatar URL of the transfer target.
*/ */
@Json(name = "avatar_url") val avatarUrl: String? @Json(name = "avatar_url") val avatarUrl: String?
) )
} }

View File

@ -35,5 +35,5 @@ object MessageType {
const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker"
const val MSGTYPE_CONFETTI = "nic.custom.confetti" const val MSGTYPE_CONFETTI = "nic.custom.confetti"
const val MSGTYPE_SNOW = "io.element.effect.snowfall" const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall"
} }

View File

@ -28,7 +28,8 @@ sealed class PeekResult {
val numJoinedMembers: Int?, val numJoinedMembers: Int?,
val roomType: String?, val roomType: String?,
val viaServers: List<String>, val viaServers: List<String>,
val someMembers: List<MatrixItem.UserItem>? val someMembers: List<MatrixItem.UserItem>?,
val isPublic: Boolean
) : PeekResult() ) : PeekResult()
data class PeekingNotAllowed( data class PeekingNotAllowed(

View File

@ -22,16 +22,16 @@ import org.matrix.android.sdk.api.util.JsonDict
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ThirdPartyUser( data class ThirdPartyUser(
/* /**
Required. A Matrix User ID represting a third party user. * Required. A Matrix User ID representing a third party user.
*/ */
@Json(name = "userid") val userId: String, @Json(name = "userid") val userId: String,
/* /**
Required. The protocol ID that the third party location is a part of. * Required. The protocol ID that the third party location is a part of.
*/ */
@Json(name = "protocol") val protocol: String, @Json(name = "protocol") val protocol: String,
/* /**
Required. Information used to identify this third party location. * Required. Information used to identify this third party location.
*/ */
@Json(name = "fields") val fields: JsonDict @Json(name = "fields") val fields: JsonDict
) )

View File

@ -20,6 +20,7 @@ import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.session.group.model.GroupSummary
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.RoomType
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom
import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.room.sender.SenderInfo
@ -38,6 +39,8 @@ sealed class MatrixItem(
init { init {
if (BuildConfig.DEBUG) checkId() if (BuildConfig.DEBUG) checkId()
} }
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
} }
data class EventItem(override val id: String, data class EventItem(override val id: String,
@ -47,6 +50,8 @@ sealed class MatrixItem(
init { init {
if (BuildConfig.DEBUG) checkId() if (BuildConfig.DEBUG) checkId()
} }
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
} }
data class RoomItem(override val id: String, data class RoomItem(override val id: String,
@ -56,6 +61,19 @@ sealed class MatrixItem(
init { init {
if (BuildConfig.DEBUG) checkId() if (BuildConfig.DEBUG) checkId()
} }
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
}
data class SpaceItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
} }
data class RoomAliasItem(override val id: String, data class RoomAliasItem(override val id: String,
@ -68,6 +86,8 @@ sealed class MatrixItem(
// Best name is the id, and we keep the displayName of the room for the case we need the first letter // Best name is the id, and we keep the displayName of the room for the case we need the first letter
override fun getBestName() = id override fun getBestName() = id
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
} }
data class GroupItem(override val id: String, data class GroupItem(override val id: String,
@ -80,6 +100,8 @@ sealed class MatrixItem(
// Best name is the id, and we keep the displayName of the room for the case we need the first letter // Best name is the id, and we keep the displayName of the room for the case we need the first letter
override fun getBestName() = id override fun getBestName() = id
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
} }
open fun getBestName(): String { open fun getBestName(): String {
@ -92,12 +114,15 @@ sealed class MatrixItem(
} }
} }
abstract fun updateAvatar(newAvatar: String?): MatrixItem
/** /**
* Return the prefix as defined in the matrix spec (and not extracted from the id) * Return the prefix as defined in the matrix spec (and not extracted from the id)
*/ */
fun getIdPrefix() = when (this) { fun getIdPrefix() = when (this) {
is UserItem -> '@' is UserItem -> '@'
is EventItem -> '$' is EventItem -> '$'
is SpaceItem,
is RoomItem -> '!' is RoomItem -> '!'
is RoomAliasItem -> '#' is RoomAliasItem -> '#'
is GroupItem -> '+' is GroupItem -> '+'
@ -148,7 +173,11 @@ fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl) fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl)
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl) fun RoomSummary.toMatrixItem() = if (roomType == RoomType.SPACE) {
MatrixItem.SpaceItem(roomId, displayName, avatarUrl)
} else {
MatrixItem.RoomItem(roomId, displayName, avatarUrl)
}
fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl) fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl)
@ -159,4 +188,8 @@ fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName,
fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)
fun SpaceChildInfo.toMatrixItem() = MatrixItem.RoomItem(childRoomId, name ?: canonicalAlias ?: "", avatarUrl) fun SpaceChildInfo.toMatrixItem() = if (roomType == RoomType.SPACE) {
MatrixItem.SpaceItem(childRoomId, name ?: canonicalAlias, avatarUrl)
} else {
MatrixItem.RoomItem(childRoomId, name ?: canonicalAlias, avatarUrl)
}

View File

@ -33,7 +33,6 @@ internal const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/"
* Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login * Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login
*/ */
internal const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect" internal const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect"
internal const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect"
internal const val SSO_REDIRECT_URL_PARAM = "redirectUrl" internal const val SSO_REDIRECT_URL_PARAM = "redirectUrl"

View File

@ -88,11 +88,9 @@ internal class DefaultAuthenticationService @Inject constructor(
return buildString { return buildString {
append(homeServerUrlBase) append(homeServerUrlBase)
append(SSO_REDIRECT_PATH)
if (providerId != null) { if (providerId != null) {
append(MSC2858_SSO_REDIRECT_PATH)
append("/$providerId") append("/$providerId")
} else {
append(SSO_REDIRECT_PATH)
} }
// Set the redirect url // Set the redirect url
appendParamToUrl(SSO_REDIRECT_URL_PARAM, redirectUrl) appendParamToUrl(SSO_REDIRECT_URL_PARAM, redirectUrl)

View File

@ -42,7 +42,7 @@ internal data class LoginFlow(
* the client can show a button for each of the supported providers * the client can show a button for each of the supported providers
* See MSC #2858 * See MSC #2858
*/ */
@Json(name = "org.matrix.msc2858.identity_providers") @Json(name = "identity_providers")
val ssoIdentityProvider: List<SsoIdentityProvider>? = null val ssoIdentityProvider: List<SsoIdentityProvider>? = null
) )

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.database.mapper package org.matrix.android.sdk.internal.database.mapper
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
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.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
@ -92,7 +93,8 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
parentRoomId = roomSummaryEntity.roomId, parentRoomId = roomSummaryEntity.roomId,
suggested = it.suggested, suggested = it.suggested,
canonicalAlias = it.childSummaryEntity?.canonicalAlias, canonicalAlias = it.childSummaryEntity?.canonicalAlias,
aliases = it.childSummaryEntity?.aliases?.toList() aliases = it.childSummaryEntity?.aliases?.toList(),
worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC
) )
}, },
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList() flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList()

View File

@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.SessionId
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
@ -43,15 +44,16 @@ internal class SessionListeners @Inject constructor(
fun dispatch(block: (Session, Session.Listener) -> Unit) { fun dispatch(block: (Session, Session.Listener) -> Unit) {
synchronized(listeners) { synchronized(listeners) {
val session = getSession() val session = getSession() ?: return Unit.also {
Timber.w("You don't have any attached session")
}
listeners.forEach { listeners.forEach {
tryOrNull { block(session, it) } tryOrNull { block(session, it) }
} }
} }
} }
private fun getSession(): Session { private fun getSession(): Session? {
return sessionManager.getSessionComponent(sessionId)?.session() return sessionManager.getSessionComponent(sessionId)?.session()
?: throw IllegalStateException("No session found with this id.")
} }
} }

View File

@ -24,18 +24,15 @@ 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.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
@ -192,6 +189,9 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
// Ignore remote echo // Ignore remote echo
return return
} }
if (event.roomId == null || event.senderId == null) {
return
}
if (event.senderId == userId) { if (event.senderId == userId) {
// discard current call, it's answered by another of my session // discard current call, it's answered by another of my session
activeCallHandler.removeCall(call.callId) activeCallHandler.removeCall(call.callId)
@ -201,11 +201,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
Timber.v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}") Timber.v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}")
return return
} }
call.apply { mxCallFactory.updateOutgoingCallWithOpponentData(call, event.senderId, content, content.capabilities)
opponentPartyId = Optional.from(content.partyId)
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
capabilities = content.capabilities ?: CallCapabilities()
}
callListenersDispatcher.onCallAnswerReceived(content) callListenersDispatcher.onCallAnswerReceived(content)
} }
} }

View File

@ -17,18 +17,17 @@
package org.matrix.android.sdk.internal.session.call package org.matrix.android.sdk.internal.session.call
import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.session.call.CallIdGenerator
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent
import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.DeviceId
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.call.model.MxCallImpl import org.matrix.android.sdk.internal.session.call.model.MxCallImpl
import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import java.math.BigDecimal
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
internal class MxCallFactory @Inject constructor( internal class MxCallFactory @Inject constructor(
@ -48,32 +47,38 @@ internal class MxCallFactory @Inject constructor(
roomId = roomId, roomId = roomId,
userId = userId, userId = userId,
ourPartyId = deviceId ?: "", ourPartyId = deviceId ?: "",
opponentUserId = opponentUserId,
isVideoCall = content.isVideo(), isVideoCall = content.isVideo(),
localEchoEventFactory = localEchoEventFactory, localEchoEventFactory = localEchoEventFactory,
eventSenderProcessor = eventSenderProcessor, eventSenderProcessor = eventSenderProcessor,
matrixConfiguration = matrixConfiguration, matrixConfiguration = matrixConfiguration,
getProfileInfoTask = getProfileInfoTask getProfileInfoTask = getProfileInfoTask
).apply { ).apply {
opponentPartyId = Optional.from(content.partyId) updateOpponentData(opponentUserId, content, content.capabilities)
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
capabilities = content.capabilities ?: CallCapabilities()
} }
} }
fun createOutgoingCall(roomId: String, opponentUserId: String, isVideoCall: Boolean): MxCall { fun createOutgoingCall(roomId: String, opponentUserId: String, isVideoCall: Boolean): MxCall {
return MxCallImpl( return MxCallImpl(
callId = UUID.randomUUID().toString(), callId = CallIdGenerator.generate(),
isOutgoing = true, isOutgoing = true,
roomId = roomId, roomId = roomId,
userId = userId, userId = userId,
ourPartyId = deviceId ?: "", ourPartyId = deviceId ?: "",
opponentUserId = opponentUserId,
isVideoCall = isVideoCall, isVideoCall = isVideoCall,
localEchoEventFactory = localEchoEventFactory, localEchoEventFactory = localEchoEventFactory,
eventSenderProcessor = eventSenderProcessor, eventSenderProcessor = eventSenderProcessor,
matrixConfiguration = matrixConfiguration, matrixConfiguration = matrixConfiguration,
getProfileInfoTask = getProfileInfoTask getProfileInfoTask = getProfileInfoTask
) ).apply {
// Setup with this userId, might be updated when processing the Answer event
this.opponentUserId = opponentUserId
}
}
fun updateOutgoingCallWithOpponentData(call: MxCall,
userId: String,
content: CallSignalingContent,
callCapabilities: CallCapabilities?) {
(call as? MxCallImpl)?.updateOpponentData(userId, content, callCapabilities)
} }
} }

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.call.model package org.matrix.android.sdk.internal.session.call.model
import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.session.call.CallIdGenerator
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
@ -36,6 +37,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent import org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent
import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.matrix.android.sdk.api.session.room.model.call.SdpType
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
@ -43,14 +45,13 @@ import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.math.BigDecimal
internal class MxCallImpl( internal class MxCallImpl(
override val callId: String, override val callId: String,
override val isOutgoing: Boolean, override val isOutgoing: Boolean,
override val roomId: String, override val roomId: String,
private val userId: String, private val userId: String,
override val opponentUserId: String,
override val isVideoCall: Boolean, override val isVideoCall: Boolean,
override val ourPartyId: String, override val ourPartyId: String,
private val localEchoEventFactory: LocalEchoEventFactory, private val localEchoEventFactory: LocalEchoEventFactory,
@ -61,8 +62,16 @@ internal class MxCallImpl(
override var opponentPartyId: Optional<String>? = null override var opponentPartyId: Optional<String>? = null
override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION
override lateinit var opponentUserId: String
override var capabilities: CallCapabilities? = null override var capabilities: CallCapabilities? = null
fun updateOpponentData(userId: String, content: CallSignalingContent, callCapabilities: CallCapabilities?) {
opponentPartyId = Optional.from(content.partyId)
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
opponentUserId = userId
capabilities = callCapabilities ?: CallCapabilities()
}
override var state: CallState = CallState.Idle override var state: CallState = CallState.Idle
set(value) { set(value) {
field = value field = value
@ -202,7 +211,10 @@ internal class MxCallImpl(
.also { eventSenderProcessor.postEvent(it) } .also { eventSenderProcessor.postEvent(it) }
} }
override suspend fun transfer(targetUserId: String, targetRoomId: String?) { override suspend fun transfer(targetUserId: String,
targetRoomId: String?,
createCallId: String?,
awaitCallId: String?) {
val profileInfoParams = GetProfileInfoTask.Params(targetUserId) val profileInfoParams = GetProfileInfoTask.Params(targetUserId)
val profileInfo = try { val profileInfo = try {
getProfileInfoTask.execute(profileInfoParams) getProfileInfoTask.execute(profileInfoParams)
@ -213,15 +225,16 @@ internal class MxCallImpl(
CallReplacesContent( CallReplacesContent(
callId = callId, callId = callId,
partyId = ourPartyId, partyId = ourPartyId,
replacementId = UUID.randomUUID().toString(), replacementId = CallIdGenerator.generate(),
version = MxCall.VOIP_PROTO_VERSION.toString(), version = MxCall.VOIP_PROTO_VERSION.toString(),
targetUser = CallReplacesContent.TargetUser( targetUser = CallReplacesContent.TargetUser(
id = targetUserId, id = targetUserId,
displayName = profileInfo?.get(ProfileService.DISPLAY_NAME_KEY) as? String, displayName = profileInfo?.get(ProfileService.DISPLAY_NAME_KEY) as? String,
avatarUrl = profileInfo?.get(ProfileService.AVATAR_URL_KEY) as? String avatarUrl = profileInfo?.get(ProfileService.AVATAR_URL_KEY) as? String
), ),
targerRoomId = targetRoomId, targetRoomId = targetRoomId,
createCall = UUID.randomUUID().toString() awaitCall = awaitCallId,
createCall = createCallId
) )
.let { createEventAndLocalEcho(type = EventType.CALL_REPLACES, roomId = roomId, content = it.toContent()) } .let { createEventAndLocalEcho(type = EventType.CALL_REPLACES, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) } .also { eventSenderProcessor.postEvent(it) }

View File

@ -52,7 +52,7 @@ internal class ThumbnailExtractor @Inject constructor(
mediaMetadataRetriever.setDataSource(context, attachment.queryUri) mediaMetadataRetriever.setDataSource(context, attachment.queryUri)
mediaMetadataRetriever.frameAtTime?.let { thumbnail -> mediaMetadataRetriever.frameAtTime?.let { thumbnail ->
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) thumbnail.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
val thumbnailWidth = thumbnail.width val thumbnailWidth = thumbnail.width
val thumbnailHeight = thumbnail.height val thumbnailHeight = thumbnail.height
val thumbnailSize = outputStream.size() val thumbnailSize = outputStream.size()

View File

@ -33,8 +33,8 @@ internal class DefaultPermalinkService @Inject constructor(
return permalinkFactory.createPermalink(id) return permalinkFactory.createPermalink(id)
} }
override fun createRoomPermalink(roomId: String): String? { override fun createRoomPermalink(roomId: String, viaServers: List<String>?): String? {
return permalinkFactory.createRoomPermalink(roomId) return permalinkFactory.createRoomPermalink(roomId, viaServers)
} }
override fun createPermalink(roomId: String, eventId: String): String { override fun createPermalink(roomId: String, eventId: String): String {

View File

@ -40,11 +40,18 @@ internal class PermalinkFactory @Inject constructor(
} else MATRIX_TO_URL_BASE + escape(id) } else MATRIX_TO_URL_BASE + escape(id)
} }
fun createRoomPermalink(roomId: String): String? { fun createRoomPermalink(roomId: String, via: List<String>? = null): String? {
return if (roomId.isEmpty()) { return if (roomId.isEmpty()) {
null null
} else { } else {
MATRIX_TO_URL_BASE + escape(roomId) + viaParameterFinder.computeViaParams(userId, roomId) buildString {
append(MATRIX_TO_URL_BASE)
append(escape(roomId))
append(
via?.takeIf { it.isNotEmpty() }?.let { viaParameterFinder.asUrlViaParameters(it) }
?: viaParameterFinder.computeViaParams(userId, roomId)
)
}
} }
} }

View File

@ -39,8 +39,11 @@ internal class ViaParameterFinder @Inject constructor(
* current user one. * current user one.
*/ */
fun computeViaParams(userId: String, roomId: String): String { fun computeViaParams(userId: String, roomId: String): String {
return computeViaParams(userId, roomId, 3) return asUrlViaParameters(computeViaParams(userId, roomId, 3))
.joinToString(prefix = "?via=", separator = "&via=") { URLEncoder.encode(it, "utf-8") } }
fun asUrlViaParameters(viaList: List<String>): String {
return viaList.joinToString(prefix = "?via=", separator = "&via=") { URLEncoder.encode(it, "utf-8") }
} }
fun computeViaParams(userId: String, roomId: String, max: Int): List<String> { fun computeViaParams(userId: String, roomId: String, max: Int): List<String> {

View File

@ -23,6 +23,8 @@ 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.RoomAvatarContent
import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent 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.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.RoomNameContent 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.RoomTopicContent
@ -105,7 +107,8 @@ internal class DefaultPeekRoomTask @Inject constructor(
numJoinedMembers = publicRepoResult.numJoinedMembers, numJoinedMembers = publicRepoResult.numJoinedMembers,
viaServers = serverList, viaServers = serverList,
roomType = null, // would be nice to get that from directory... roomType = null, // would be nice to get that from directory...
someMembers = null someMembers = null,
isPublic = true
) )
} }
@ -143,6 +146,11 @@ internal class DefaultPeekRoomTask @Inject constructor(
} }
} }
val historyVisibility =
stateEvents
.lastOrNull { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY && it.stateKey?.isNotEmpty() == true }
?.let { it.content?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility }
val roomType = stateEvents val roomType = stateEvents
.lastOrNull { it.type == EventType.STATE_ROOM_CREATE } .lastOrNull { it.type == EventType.STATE_ROOM_CREATE }
?.content ?.content
@ -158,7 +166,8 @@ internal class DefaultPeekRoomTask @Inject constructor(
numJoinedMembers = memberCount, numJoinedMembers = memberCount,
roomType = roomType, roomType = roomType,
viaServers = serverList, viaServers = serverList,
someMembers = someMembers someMembers = someMembers,
isPublic = historyVisibility == RoomHistoryVisibility.WORLD_READABLE
) )
} catch (failure: Throwable) { } catch (failure: Throwable) {
// Would be M_FORBIDDEN if cannot peek :/ // Would be M_FORBIDDEN if cannot peek :/

View File

@ -147,7 +147,8 @@ internal class DefaultSpaceService @Inject constructor(
parentRoomId = childStateEv.roomId, parentRoomId = childStateEv.roomId,
suggested = childStateEvContent.suggested, suggested = childStateEvContent.suggested,
canonicalAlias = childSummary.canonicalAlias, canonicalAlias = childSummary.canonicalAlias,
aliases = childSummary.aliases aliases = childSummary.aliases,
worldReadable = childSummary.worldReadable
) )
} }
}.orEmpty() }.orEmpty()

View File

@ -0,0 +1 @@
Allow user to add custom "network" in room search

View File

@ -0,0 +1 @@
Add Gitter.im as a default in the Change Network menu

View File

@ -0,0 +1 @@
Compress thumbnail: change Jpeg quality from 100 to 80

1
newsfragment/3401.bugfix Normal file
View File

@ -0,0 +1 @@
Fix | On Android it seems to be impossible to view the complete description of a Space (without dev tools)

1
newsfragment/3406.bugfix Normal file
View File

@ -0,0 +1 @@
Fix | Suggest Rooms, Show a detailed view of the room on click

View File

@ -0,0 +1 @@
VoIP: support attended transfer

1
newsfragment/3424.bugfix Normal file
View File

@ -0,0 +1 @@
Fix app crashing when signing out

View File

@ -0,0 +1 @@
/snow -> /snowfall and update wording (iso Element Web)

1
newsfragment/3442.bugfix Normal file
View File

@ -0,0 +1 @@
Switch to stable endpoint/fields for MSC2858

View File

@ -94,6 +94,10 @@ fun <T : Fragment> AppCompatActivity.addFragmentToBackstack(
} }
} }
fun AppCompatActivity.popBackstack() {
supportFragmentManager.popBackStack()
}
fun AppCompatActivity.resetBackstack() { fun AppCompatActivity.resetBackstack() {
repeat(supportFragmentManager.backStackEntryCount) { repeat(supportFragmentManager.backStackEntryCount) {
supportFragmentManager.popBackStack() supportFragmentManager.popBackStack()

View File

@ -0,0 +1,45 @@
/*
* Copyright 2021 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.core.ui.list
import android.view.View
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
/**
* A generic item with empty space.
*/
@EpoxyModelClass(layout = R.layout.item_vertical_margin)
abstract class VerticalMarginItem : VectorEpoxyModel<VerticalMarginItem.Holder>() {
@EpoxyAttribute
var heightInPx: Int = 0
override fun bind(holder: Holder) {
super.bind(holder)
holder.space.updateLayoutParams {
height = heightInPx
}
}
class Holder : VectorEpoxyHolder() {
val space by bind<View>(R.id.item_vertical_margin_space)
}
}

View File

@ -175,7 +175,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
when (callState) { when (callState) {
is CallState.Idle, is CallState.Idle,
is CallState.CreateOffer, is CallState.CreateOffer,
is CallState.Dialing -> { is CallState.Dialing -> {
views.callVideoGroup.isInvisible = true views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
views.callStatusText.setText(R.string.call_ring) views.callStatusText.setText(R.string.call_ring)
@ -189,16 +189,27 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
configureCallInfo(state) configureCallInfo(state)
} }
is CallState.Answering -> { is CallState.Answering -> {
views.callVideoGroup.isInvisible = true views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
views.callStatusText.setText(R.string.call_connecting) views.callStatusText.setText(R.string.call_connecting)
views.callConnectingProgress.isVisible = true views.callConnectingProgress.isVisible = true
configureCallInfo(state) configureCallInfo(state)
} }
is CallState.Connected -> { is CallState.Connected -> {
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (state.isLocalOnHold || state.isRemoteOnHold) { if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) {
val transfereeName = if (state.transferee is VectorCallViewState.TransfereeState.KnownTransferee) {
state.transferee.name
} else {
getString(R.string.call_transfer_unknown_person)
}
views.callActionText.text = getString(R.string.call_transfer_transfer_to_title, transfereeName)
views.callActionText.isVisible = true
views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.TransferCall) }
views.callStatusText.text = state.formattedDuration
configureCallInfo(state)
} else if (state.isLocalOnHold || state.isRemoteOnHold) {
views.smallIsHeldIcon.isVisible = true views.smallIsHeldIcon.isVisible = true
views.callVideoGroup.isInvisible = true views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
@ -220,7 +231,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
if (callArgs.isVideoCall) { if (callArgs.isVideoCall) {
views.callVideoGroup.isVisible = true views.callVideoGroup.isVisible = true
views.callInfoGroup.isVisible = false views.callInfoGroup.isVisible = false
views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null
} else { } else {
views.callVideoGroup.isInvisible = true views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
@ -235,10 +246,10 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.callConnectingProgress.isVisible = true views.callConnectingProgress.isVisible = true
} }
} }
is CallState.Terminated -> { is CallState.Terminated -> {
finish() finish()
} }
null -> { null -> {
} }
} }
} }
@ -247,7 +258,11 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
state.callInfo.otherUserItem?.let { state.callInfo.otherUserItem?.let {
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen)
avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter) avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter)
views.participantNameText.text = it.getBestName() if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) {
views.participantNameText.text = it.getBestName()
} else {
views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName())
}
if (blurAvatar) { if (blurAvatar) {
avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter) avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter)
} else { } else {
@ -322,13 +337,13 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
private fun handleViewEvents(event: VectorCallViewEvents?) { private fun handleViewEvents(event: VectorCallViewEvents?) {
Timber.v("## VOIP handleViewEvents $event") Timber.v("## VOIP handleViewEvents $event")
when (event) { when (event) {
VectorCallViewEvents.DismissNoCall -> { VectorCallViewEvents.DismissNoCall -> {
finish() finish()
} }
is VectorCallViewEvents.ConnectionTimeout -> { is VectorCallViewEvents.ConnectionTimeout -> {
onErrorTimoutConnect(event.turn) onErrorTimoutConnect(event.turn)
} }
is VectorCallViewEvents.ShowDialPad -> { is VectorCallViewEvents.ShowDialPad -> {
CallDialPadBottomSheet.newInstance(false).apply { CallDialPadBottomSheet.newInstance(false).apply {
callback = dialPadCallback callback = dialPadCallback
}.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG) }.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG)
@ -336,7 +351,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
is VectorCallViewEvents.ShowCallTransferScreen -> { is VectorCallViewEvents.ShowCallTransferScreen -> {
navigator.openCallTransfer(this, callArgs.callId) navigator.openCallTransfer(this, callArgs.callId)
} }
null -> { null -> {
} }
} }
} }

View File

@ -34,4 +34,5 @@ sealed class VectorCallViewActions : VectorViewModelAction {
object ToggleCamera : VectorCallViewActions() object ToggleCamera : VectorCallViewActions()
object ToggleHDSD : VectorCallViewActions() object ToggleHDSD : VectorCallViewActions()
object InitiateCallTransfer : VectorCallViewActions() object InitiateCallTransfer : VectorCallViewActions()
object TransferCall: VectorCallViewActions()
} }

View File

@ -23,8 +23,8 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.audio.CallAudioManager
@ -111,12 +111,21 @@ class VectorCallViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
callState = Success(callState), callState = Success(callState),
canOpponentBeTransferred = call.capabilities.supportCallTransfer() canOpponentBeTransferred = call.capabilities.supportCallTransfer(),
transferee = computeTransfereeState(call)
) )
} }
} }
} }
private fun computeTransfereeState(call: MxCall): VectorCallViewState.TransfereeState {
val transfereeCall = callManager.getTransfereeForCallId(call.callId) ?: return VectorCallViewState.TransfereeState.NoTransferee
val transfereeRoom = session.getRoomSummary(transfereeCall.nativeRoomId)
return transfereeRoom?.displayName?.let {
VectorCallViewState.TransfereeState.KnownTransferee(it)
} ?: VectorCallViewState.TransfereeState.UnknownTransferee
}
private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { private val currentCallListener = object : WebRtcCallManager.CurrentCallListener {
override fun onCurrentCallChange(call: WebRtcCall?) { override fun onCurrentCallChange(call: WebRtcCall?) {
@ -166,7 +175,7 @@ class VectorCallViewModel @AssistedInject constructor(
} else { } else {
call = webRtcCall call = webRtcCall
callManager.addCurrentCallListener(currentCallListener) callManager.addCurrentCallListener(currentCallListener)
val item = webRtcCall.getOpponentAsMatrixItem(session) val item = webRtcCall.getOpponentAsMatrixItem(session)
webRtcCall.addListener(callListener) webRtcCall.addListener(callListener)
val currentSoundDevice = callManager.audioManager.selectedDevice val currentSoundDevice = callManager.audioManager.selectedDevice
if (currentSoundDevice == CallAudioManager.Device.PHONE) { if (currentSoundDevice == CallAudioManager.Device.PHONE) {
@ -185,7 +194,8 @@ class VectorCallViewModel @AssistedInject constructor(
canSwitchCamera = webRtcCall.canSwitchCamera(), canSwitchCamera = webRtcCall.canSwitchCamera(),
formattedDuration = webRtcCall.formattedDuration(), formattedDuration = webRtcCall.formattedDuration(),
isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD, isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD,
canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer() canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer(),
transferee = computeTransfereeState(webRtcCall.mxCall)
) )
} }
updateOtherKnownCall(webRtcCall) updateOtherKnownCall(webRtcCall)
@ -201,27 +211,27 @@ class VectorCallViewModel @AssistedInject constructor(
override fun handle(action: VectorCallViewActions) = withState { state -> override fun handle(action: VectorCallViewActions) = withState { state ->
when (action) { when (action) {
VectorCallViewActions.EndCall -> call?.endCall() VectorCallViewActions.EndCall -> call?.endCall()
VectorCallViewActions.AcceptCall -> { VectorCallViewActions.AcceptCall -> {
setState { setState {
copy(callState = Loading()) copy(callState = Loading())
} }
call?.acceptIncomingCall() call?.acceptIncomingCall()
} }
VectorCallViewActions.DeclineCall -> { VectorCallViewActions.DeclineCall -> {
setState { setState {
copy(callState = Loading()) copy(callState = Loading())
} }
call?.endCall() call?.endCall()
} }
VectorCallViewActions.ToggleMute -> { VectorCallViewActions.ToggleMute -> {
val muted = state.isAudioMuted val muted = state.isAudioMuted
call?.muteCall(!muted) call?.muteCall(!muted)
setState { setState {
copy(isAudioMuted = !muted) copy(isAudioMuted = !muted)
} }
} }
VectorCallViewActions.ToggleVideo -> { VectorCallViewActions.ToggleVideo -> {
if (state.isVideoCall) { if (state.isVideoCall) {
val videoEnabled = state.isVideoEnabled val videoEnabled = state.isVideoEnabled
call?.enableVideo(!videoEnabled) call?.enableVideo(!videoEnabled)
@ -231,14 +241,14 @@ class VectorCallViewModel @AssistedInject constructor(
} }
Unit Unit
} }
VectorCallViewActions.ToggleHoldResume -> { VectorCallViewActions.ToggleHoldResume -> {
val isRemoteOnHold = state.isRemoteOnHold val isRemoteOnHold = state.isRemoteOnHold
call?.updateRemoteOnHold(!isRemoteOnHold) call?.updateRemoteOnHold(!isRemoteOnHold)
} }
is VectorCallViewActions.ChangeAudioDevice -> { is VectorCallViewActions.ChangeAudioDevice -> {
callManager.audioManager.setAudioDevice(action.device) callManager.audioManager.setAudioDevice(action.device)
} }
VectorCallViewActions.SwitchSoundDevice -> { VectorCallViewActions.SwitchSoundDevice -> {
_viewEvents.post( _viewEvents.post(
VectorCallViewEvents.ShowSoundDeviceChooser(state.availableDevices, state.device) VectorCallViewEvents.ShowSoundDeviceChooser(state.availableDevices, state.device)
) )
@ -254,17 +264,17 @@ class VectorCallViewModel @AssistedInject constructor(
} }
Unit Unit
} }
VectorCallViewActions.ToggleCamera -> { VectorCallViewActions.ToggleCamera -> {
call?.switchCamera() call?.switchCamera()
} }
VectorCallViewActions.ToggleHDSD -> { VectorCallViewActions.ToggleHDSD -> {
if (!state.isVideoCall) return@withState if (!state.isVideoCall) return@withState
call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD) call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD)
} }
VectorCallViewActions.OpenDialPad -> { VectorCallViewActions.OpenDialPad -> {
_viewEvents.post(VectorCallViewEvents.ShowDialPad) _viewEvents.post(VectorCallViewEvents.ShowDialPad)
} }
is VectorCallViewActions.SendDtmfDigit -> { is VectorCallViewActions.SendDtmfDigit -> {
call?.sendDtmfDigit(action.digit) call?.sendDtmfDigit(action.digit)
} }
VectorCallViewActions.InitiateCallTransfer -> { VectorCallViewActions.InitiateCallTransfer -> {
@ -272,9 +282,20 @@ class VectorCallViewModel @AssistedInject constructor(
VectorCallViewEvents.ShowCallTransferScreen VectorCallViewEvents.ShowCallTransferScreen
) )
} }
VectorCallViewActions.TransferCall -> {
handleCallTransfer()
}
}.exhaustive }.exhaustive
} }
private fun handleCallTransfer() {
viewModelScope.launch {
val currentCall = call ?: return@launch
val transfereeCall = callManager.getTransfereeForCallId(currentCall.callId) ?: return@launch
currentCall.transferToCall(transfereeCall)
}
}
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(initialState: VectorCallViewState): VectorCallViewModel fun create(initialState: VectorCallViewState): VectorCallViewModel

View File

@ -41,15 +41,22 @@ data class VectorCallViewState(
val otherKnownCallInfo: CallInfo? = null, val otherKnownCallInfo: CallInfo? = null,
val callInfo: CallInfo = CallInfo(callId), val callInfo: CallInfo = CallInfo(callId),
val formattedDuration: String = "", val formattedDuration: String = "",
val canOpponentBeTransferred: Boolean = false val canOpponentBeTransferred: Boolean = false,
val transferee: TransfereeState = TransfereeState.NoTransferee
) : MvRxState { ) : MvRxState {
sealed class TransfereeState {
object NoTransferee : TransfereeState()
data class KnownTransferee(val name: String) : TransfereeState()
object UnknownTransferee : TransfereeState()
}
data class CallInfo( data class CallInfo(
val callId: String, val callId: String,
val otherUserItem: MatrixItem? = null val otherUserItem: MatrixItem? = null
) )
constructor(callArgs: CallArgs): this( constructor(callArgs: CallArgs) : this(
callId = callArgs.callId, callId = callArgs.callId,
roomId = callArgs.signalingRoomId, roomId = callArgs.signalingRoomId,
isVideoCall = callArgs.isVideoCall isVideoCall = callArgs.isVideoCall

View File

@ -28,13 +28,16 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState, class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
private val dialPadLookup: DialPadLookup, private val dialPadLookup: DialPadLookup,
callManager: WebRtcCallManager) private val directRoomHelper: DirectRoomHelper,
private val callManager: WebRtcCallManager)
: VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) { : VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -75,7 +78,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
override fun handle(action: CallTransferAction) { override fun handle(action: CallTransferAction) {
when (action) { when (action) {
is CallTransferAction.ConnectWithUserId -> connectWithUserId(action) is CallTransferAction.ConnectWithUserId -> connectWithUserId(action)
is CallTransferAction.ConnectWithPhoneNumber -> connectWithPhoneNumber(action) is CallTransferAction.ConnectWithPhoneNumber -> connectWithPhoneNumber(action)
}.exhaustive }.exhaustive
} }
@ -83,8 +86,17 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) { private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) {
viewModelScope.launch { viewModelScope.launch {
try { try {
_viewEvents.post(CallTransferViewEvents.Loading) if (action.consultFirst) {
call?.mxCall?.transfer(action.selectedUserId, null) val dmRoomId = directRoomHelper.ensureDMExists(action.selectedUserId)
callManager.startOutgoingCall(
nativeRoomId = dmRoomId,
otherUserId = action.selectedUserId,
isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
transferee = call
)
} else {
call?.transferToUser(action.selectedUserId, null)
}
_viewEvents.post(CallTransferViewEvents.Dismiss) _viewEvents.post(CallTransferViewEvents.Dismiss)
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(CallTransferViewEvents.FailToTransfer) _viewEvents.post(CallTransferViewEvents.FailToTransfer)
@ -97,7 +109,16 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
try { try {
_viewEvents.post(CallTransferViewEvents.Loading) _viewEvents.post(CallTransferViewEvents.Loading)
val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber) val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
call?.mxCall?.transfer(result.userId, result.roomId) if (action.consultFirst) {
callManager.startOutgoingCall(
nativeRoomId = result.roomId,
otherUserId = result.userId,
isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
transferee = call
)
} else {
call?.transferToUser(result.userId, result.roomId)
}
_viewEvents.post(CallTransferViewEvents.Dismiss) _viewEvents.post(CallTransferViewEvents.Dismiss)
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(CallTransferViewEvents.FailToTransfer) _viewEvents.post(CallTransferViewEvents.FailToTransfer)

View File

@ -45,6 +45,7 @@ import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallIdGenerator
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
@ -85,16 +86,19 @@ private const val AUDIO_TRACK_ID = "ARDAMSa0"
private const val VIDEO_TRACK_ID = "ARDAMSv0" private const val VIDEO_TRACK_ID = "ARDAMSv0"
private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints() private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints()
class WebRtcCall(val mxCall: MxCall, class WebRtcCall(
// This is where the call is placed from an ui perspective. In case of virtual room, it can differs from the signalingRoomId. val mxCall: MxCall,
val nativeRoomId: String, // This is where the call is placed from an ui perspective.
private val rootEglBase: EglBase?, // In case of virtual room, it can differs from the signalingRoomId.
private val context: Context, val nativeRoomId: String,
private val dispatcher: CoroutineContext, private val rootEglBase: EglBase?,
private val sessionProvider: Provider<Session?>, private val context: Context,
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>, private val dispatcher: CoroutineContext,
private val onCallBecomeActive: (WebRtcCall) -> Unit, private val sessionProvider: Provider<Session?>,
private val onCallEnded: (String) -> Unit) : MxCall.StateListener { private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
private val onCallBecomeActive: (WebRtcCall) -> Unit,
private val onCallEnded: (String) -> Unit
) : MxCall.StateListener {
interface Listener : MxCall.StateListener { interface Listener : MxCall.StateListener {
fun onCaptureStateChanged() {} fun onCaptureStateChanged() {}
@ -118,6 +122,7 @@ class WebRtcCall(val mxCall: MxCall,
} }
val callId = mxCall.callId val callId = mxCall.callId
// room where call signaling is placed. In case of virtual room it can differs from the nativeRoomId. // room where call signaling is placed. In case of virtual room it can differs from the nativeRoomId.
val signalingRoomId = mxCall.roomId val signalingRoomId = mxCall.roomId
@ -271,7 +276,7 @@ class WebRtcCall(val mxCall: MxCall,
sessionScope?.launch(dispatcher) { sessionScope?.launch(dispatcher) {
when (mode) { when (mode) {
VectorCallActivity.INCOMING_ACCEPT -> { VectorCallActivity.INCOMING_ACCEPT -> {
internalAcceptIncomingCall() internalAcceptIncomingCall()
} }
VectorCallActivity.INCOMING_RINGING -> { VectorCallActivity.INCOMING_RINGING -> {
@ -289,6 +294,40 @@ class WebRtcCall(val mxCall: MxCall,
} }
} }
/**
* Without consultation
*/
suspend fun transferToUser(targetUserId: String, targetRoomId: String?) {
mxCall.transfer(
targetUserId = targetUserId,
targetRoomId = targetRoomId,
createCallId = CallIdGenerator.generate(),
awaitCallId = null
)
endCall(sendEndSignaling = false)
}
/**
* With consultation
*/
suspend fun transferToCall(transferTargetCall: WebRtcCall) {
val newCallId = CallIdGenerator.generate()
transferTargetCall.mxCall.transfer(
targetUserId = mxCall.opponentUserId,
targetRoomId = null,
createCallId = null,
awaitCallId = newCallId
)
mxCall.transfer(
targetUserId = transferTargetCall.mxCall.opponentUserId,
targetRoomId = null,
createCallId = newCallId,
awaitCallId = null
)
endCall(sendEndSignaling = false)
transferTargetCall.endCall(sendEndSignaling = false)
}
fun acceptIncomingCall() { fun acceptIncomingCall() {
sessionScope?.launch { sessionScope?.launch {
Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}") Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}")
@ -729,7 +768,7 @@ class WebRtcCall(val mxCall: MxCall,
} }
} }
fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) { fun endCall(sendEndSignaling: Boolean = true, reason: CallHangupContent.Reason? = null) {
if (mxCall.state == CallState.Terminated) { if (mxCall.state == CallState.Terminated) {
return return
} }
@ -744,9 +783,9 @@ class WebRtcCall(val mxCall: MxCall,
mxCall.state = CallState.Terminated mxCall.state = CallState.Terminated
sessionScope?.launch(dispatcher) { sessionScope?.launch(dispatcher) {
release() release()
onCallEnded(callId)
} }
onCallEnded(callId) if (sendEndSignaling) {
if (originatedByMe) {
if (wasRinging) { if (wasRinging) {
mxCall.reject() mxCall.reject()
} else { } else {

View File

@ -147,6 +147,11 @@ class WebRtcCallManager @Inject constructor(
private val callsByCallId = ConcurrentHashMap<String, WebRtcCall>() private val callsByCallId = ConcurrentHashMap<String, WebRtcCall>()
private val callsByRoomId = ConcurrentHashMap<String, MutableList<WebRtcCall>>() private val callsByRoomId = ConcurrentHashMap<String, MutableList<WebRtcCall>>()
// Calls started as an attended transfer, ie. with the intention of transferring another
// call with a different party to this one.
// callId (target) -> call (transferee)
private val transferees = ConcurrentHashMap<String, WebRtcCall>()
fun getCallById(callId: String): WebRtcCall? { fun getCallById(callId: String): WebRtcCall? {
return callsByCallId[callId] return callsByCallId[callId]
} }
@ -155,6 +160,10 @@ class WebRtcCallManager @Inject constructor(
return callsByRoomId[roomId] ?: emptyList() return callsByRoomId[roomId] ?: emptyList()
} }
fun getTransfereeForCallId(callId: String): WebRtcCall? {
return transferees[callId]
}
fun getCurrentCall(): WebRtcCall? { fun getCurrentCall(): WebRtcCall? {
return currentCall.get() return currentCall.get()
} }
@ -229,34 +238,31 @@ class WebRtcCallManager @Inject constructor(
CallService.onCallTerminated(context, callId) CallService.onCallTerminated(context, callId)
callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall) callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall)
callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall) callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall)
transferees.remove(callId)
if (getCurrentCall()?.callId == callId) { if (getCurrentCall()?.callId == callId) {
val otherCall = getCalls().lastOrNull() val otherCall = getCalls().lastOrNull()
currentCall.setAndNotify(otherCall) currentCall.setAndNotify(otherCall)
} }
// This must be done in this thread // There is no active calls
executor.execute { if (getCurrentCall() == null) {
// There is no active calls Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one")
if (getCurrentCall() == null) { peerConnectionFactory?.dispose()
Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one") peerConnectionFactory = null
peerConnectionFactory?.dispose() audioManager.setMode(CallAudioManager.Mode.DEFAULT)
peerConnectionFactory = null // did we start background sync? so we should stop it
audioManager.setMode(CallAudioManager.Mode.DEFAULT) if (isInBackground) {
// did we start background sync? so we should stop it if (UPHelper.hasEndpoint(context)) {
if (isInBackground) { currentSession?.stopAnyBackgroundSync()
if (UPHelper.hasEndpoint(context)) { } else {
currentSession?.stopAnyBackgroundSync() // for fdroid we should not stop, it should continue syncing
} else { // maybe we should restore default timeout/delay though?
// for fdroid we should not stop, it should continue syncing
// maybe we should restore default timeout/delay though?
}
} }
} }
Timber.v("## VOIP WebRtcPeerConnectionManager close() executor done")
} }
} }
suspend fun startOutgoingCall(nativeRoomId: String, otherUserId: String, isVideoCall: Boolean) { suspend fun startOutgoingCall(nativeRoomId: String, otherUserId: String, isVideoCall: Boolean, transferee: WebRtcCall? = null) {
val signalingRoomId = callUserMapper?.getOrCreateVirtualRoomForRoom(nativeRoomId, otherUserId) ?: nativeRoomId val signalingRoomId = callUserMapper?.getOrCreateVirtualRoomForRoom(nativeRoomId, otherUserId) ?: nativeRoomId
Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
if (getCallsByRoomId(nativeRoomId).isNotEmpty()) { if (getCallsByRoomId(nativeRoomId).isNotEmpty()) {
Timber.w("## VOIP you already have a call in this room") Timber.w("## VOIP you already have a call in this room")
@ -274,7 +280,9 @@ class WebRtcCallManager @Inject constructor(
val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
val webRtcCall = createWebRtcCall(mxCall, nativeRoomId) val webRtcCall = createWebRtcCall(mxCall, nativeRoomId)
currentCall.setAndNotify(webRtcCall) currentCall.setAndNotify(webRtcCall)
if (transferee != null) {
transferees[webRtcCall.callId] = transferee
}
CallService.onOutgoingCallRinging( CallService.onOutgoingCallRinging(
context = context.applicationContext, context = context.applicationContext,
callId = mxCall.callId) callId = mxCall.callId)

View File

@ -46,7 +46,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
PLAIN("/plain", "<message>", R.string.command_description_plain, false), PLAIN("/plain", "<message>", R.string.command_description_plain, false),
DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false), DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false),
CONFETTI("/confetti", "<message>", R.string.command_confetti, false), CONFETTI("/confetti", "<message>", R.string.command_confetti, false),
SNOW("/snow", "<message>", R.string.command_snow, false), SNOWFALL("/snowfall", "<message>", R.string.command_snow, false),
CREATE_SPACE("/createspace", "<name> <invitee>*", R.string.command_description_create_space, true), CREATE_SPACE("/createspace", "<name> <invitee>*", R.string.command_description_create_space, true),
ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space, true), ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_space, true),
JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true), JOIN_SPACE("/joinSpace", "spaceId", R.string.command_description_join_space, true),

View File

@ -296,9 +296,9 @@ object CommandParser {
val message = textMessage.substring(Command.CONFETTI.command.length).trim() val message = textMessage.substring(Command.CONFETTI.command.length).trim()
ParsedCommand.SendChatEffect(ChatEffect.CONFETTI, message) ParsedCommand.SendChatEffect(ChatEffect.CONFETTI, message)
} }
Command.SNOW.command -> { Command.SNOWFALL.command -> {
val message = textMessage.substring(Command.SNOW.command.length).trim() val message = textMessage.substring(Command.SNOWFALL.command.length).trim()
ParsedCommand.SendChatEffect(ChatEffect.SNOW, message) ParsedCommand.SendChatEffect(ChatEffect.SNOWFALL, message)
} }
Command.CREATE_SPACE.command -> { Command.CREATE_SPACE.command -> {
val rawCommand = textMessage.substring(Command.CREATE_SPACE.command.length).trim() val rawCommand = textMessage.substring(Command.CREATE_SPACE.command.length).trim()

View File

@ -33,6 +33,9 @@ abstract class SettingsContinueCancelItem : EpoxyModelWithHolder<SettingsContinu
@EpoxyAttribute @EpoxyAttribute
var continueOnClick: ClickListener? = null var continueOnClick: ClickListener? = null
@EpoxyAttribute
var canContinue: Boolean = true
@EpoxyAttribute @EpoxyAttribute
var cancelOnClick: ClickListener? = null var cancelOnClick: ClickListener? = null
@ -43,6 +46,7 @@ abstract class SettingsContinueCancelItem : EpoxyModelWithHolder<SettingsContinu
continueText?.let { holder.continueButton.text = it } continueText?.let { holder.continueButton.text = it }
holder.continueButton.onClick(continueOnClick) holder.continueButton.onClick(continueOnClick)
holder.continueButton.isEnabled = canContinue
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View File

@ -19,6 +19,7 @@ package im.vector.app.features.form
import android.text.Editable import android.text.Editable
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
@ -52,7 +53,7 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
var inputType: Int? = null var inputType: Int? = null
@EpoxyAttribute @EpoxyAttribute
var singleLine: Boolean? = null var singleLine: Boolean = true
@EpoxyAttribute @EpoxyAttribute
var imeOptions: Int? = null var imeOptions: Int? = null
@ -60,9 +61,13 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var endIconMode: Int? = null var endIconMode: Int? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) // FIXME restore EpoxyAttribute.Option.DoNotHash and fix that properly
@EpoxyAttribute
var onTextChange: ((String) -> Unit)? = null var onTextChange: ((String) -> Unit)? = null
@EpoxyAttribute
var editorActionListener: TextView.OnEditorActionListener? = null
private val onTextChangeListener = object : SimpleTextWatcher() { private val onTextChangeListener = object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) { override fun afterTextChanged(s: Editable) {
onTextChange?.invoke(s.toString()) onTextChange?.invoke(s.toString())
@ -80,10 +85,11 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
holder.textInputEditText.isEnabled = enabled holder.textInputEditText.isEnabled = enabled
inputType?.let { holder.textInputEditText.inputType = it } inputType?.let { holder.textInputEditText.inputType = it }
holder.textInputEditText.isSingleLine = singleLine ?: false holder.textInputEditText.isSingleLine = singleLine
holder.textInputEditText.imeOptions = imeOptions ?: EditorInfo.IME_ACTION_NONE holder.textInputEditText.imeOptions = imeOptions ?: EditorInfo.IME_ACTION_NONE
holder.textInputEditText.addTextChangedListener(onTextChangeListener) holder.textInputEditText.addTextChangedListener(onTextChangeListener)
holder.textInputEditText.setOnEditorActionListener(editorActionListener)
holder.bottomSeparator.isVisible = showBottomSeparator holder.bottomSeparator.isVisible = showBottomSeparator
} }

View File

@ -71,7 +71,7 @@ abstract class FormEditableSquareAvatarItem : EpoxyModelWithHolder<FormEditableS
.into(holder.image) .into(holder.image)
} }
matrixItem != null -> { matrixItem != null -> {
avatarRenderer?.renderSpace(matrixItem!!, holder.image) avatarRenderer?.render(matrixItem!!, holder.image)
} }
else -> { else -> {
avatarRenderer?.clear(holder.image) avatarRenderer?.clear(holder.image)

View File

@ -66,24 +66,24 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
DrawableImageViewTarget(imageView)) DrawableImageViewTarget(imageView))
} }
@UiThread // fun renderSpace(matrixItem: MatrixItem, imageView: ImageView) {
fun renderSpace(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) { // renderSpace(
val placeholder = getSpacePlaceholderDrawable(matrixItem) // matrixItem,
val resolvedUrl = resolvedUrl(matrixItem.avatarUrl) // imageView,
glideRequests // GlideApp.with(imageView)
.load(resolvedUrl) // )
.transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8)))) // }
.placeholder(placeholder) //
.into(DrawableImageViewTarget(imageView)) // @UiThread
} // private fun renderSpace(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) {
// val placeholder = getSpacePlaceholderDrawable(matrixItem)
fun renderSpace(matrixItem: MatrixItem, imageView: ImageView) { // val resolvedUrl = resolvedUrl(matrixItem.avatarUrl)
renderSpace( // glideRequests
matrixItem, // .load(resolvedUrl)
imageView, // .transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8))))
GlideApp.with(imageView) // .placeholder(placeholder)
) // .into(DrawableImageViewTarget(imageView))
} // }
fun clear(imageView: ImageView) { fun clear(imageView: ImageView) {
// It can be called after recycler view is destroyed, just silently catch // It can be called after recycler view is destroyed, just silently catch
@ -137,7 +137,16 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
target: Target<Drawable>) { target: Target<Drawable>) {
val placeholder = getPlaceholderDrawable(matrixItem) val placeholder = getPlaceholderDrawable(matrixItem)
buildGlideRequest(glideRequests, matrixItem.avatarUrl) buildGlideRequest(glideRequests, matrixItem.avatarUrl)
.apply(RequestOptions.circleCropTransform()) .apply {
when (matrixItem) {
is MatrixItem.SpaceItem -> {
transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8))))
}
else -> {
apply(RequestOptions.circleCropTransform())
}
}
}
.placeholder(placeholder) .placeholder(placeholder)
.into(target) .into(target)
} }
@ -197,17 +206,16 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
.beginConfig() .beginConfig()
.bold() .bold()
.endConfig() .endConfig()
.buildRound(matrixItem.firstLetterOfDisplayName(), avatarColor) .let {
} when (matrixItem) {
is MatrixItem.SpaceItem -> {
@AnyThread it.buildRoundRect(matrixItem.firstLetterOfDisplayName(), avatarColor, dimensionConverter.dpToPx(8))
fun getSpacePlaceholderDrawable(matrixItem: MatrixItem): Drawable { }
val avatarColor = matrixItemColorProvider.getColor(matrixItem) else -> {
return TextDrawable.builder() it.buildRound(matrixItem.firstLetterOfDisplayName(), avatarColor)
.beginConfig() }
.bold() }
.endConfig() }
.buildRoundRect(matrixItem.firstLetterOfDisplayName(), avatarColor, dimensionConverter.dpToPx(8))
} }
// PRIVATE API ********************************************************************************* // PRIVATE API *********************************************************************************

View File

@ -26,13 +26,13 @@ import javax.inject.Inject
enum class ChatEffect { enum class ChatEffect {
CONFETTI, CONFETTI,
SNOW SNOWFALL
} }
fun ChatEffect.toMessageType(): String { fun ChatEffect.toMessageType(): String {
return when (this) { return when (this) {
ChatEffect.CONFETTI -> MessageType.MSGTYPE_CONFETTI ChatEffect.CONFETTI -> MessageType.MSGTYPE_CONFETTI
ChatEffect.SNOW -> MessageType.MSGTYPE_SNOW ChatEffect.SNOWFALL -> MessageType.MSGTYPE_SNOWFALL
} }
} }
@ -112,14 +112,14 @@ class ChatEffectManager @Inject constructor() {
private fun findEffect(content: MessageContent, event: TimelineEvent): ChatEffect? { private fun findEffect(content: MessageContent, event: TimelineEvent): ChatEffect? {
return when (content.msgType) { return when (content.msgType) {
MessageType.MSGTYPE_CONFETTI -> ChatEffect.CONFETTI MessageType.MSGTYPE_CONFETTI -> ChatEffect.CONFETTI
MessageType.MSGTYPE_SNOW -> ChatEffect.SNOW MessageType.MSGTYPE_SNOWFALL -> ChatEffect.SNOWFALL
MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_TEXT -> { MessageType.MSGTYPE_TEXT -> {
event.root.getClearContent().toModel<MessageContent>()?.body event.root.getClearContent().toModel<MessageContent>()?.body
?.let { text -> ?.let { text ->
when { when {
EMOJIS_FOR_CONFETTI.any { text.contains(it) } -> ChatEffect.CONFETTI EMOJIS_FOR_CONFETTI.any { text.contains(it) } -> ChatEffect.CONFETTI
EMOJIS_FOR_SNOW.any { text.contains(it) } -> ChatEffect.SNOW EMOJIS_FOR_SNOWFALL.any { text.contains(it) } -> ChatEffect.SNOWFALL
else -> null else -> null
} }
} }
@ -133,7 +133,7 @@ class ChatEffectManager @Inject constructor() {
"🎉", "🎉",
"🎊" "🎊"
) )
private val EMOJIS_FOR_SNOW = listOf( private val EMOJIS_FOR_SNOWFALL = listOf(
"⛄️", "⛄️",
"☃️", "☃️",
"❄️" "❄️"

View File

@ -438,7 +438,7 @@ class RoomDetailFragment @Inject constructor(
.setPosition(-50f, views.viewKonfetti.width + 50f, -50f, -50f) .setPosition(-50f, views.viewKonfetti.width + 50f, -50f, -50f)
.streamFor(150, 3000L) .streamFor(150, 3000L)
} }
ChatEffect.SNOW -> { ChatEffect.SNOWFALL -> {
views.viewSnowFall.isVisible = true views.viewSnowFall.isVisible = true
views.viewSnowFall.restartFalling() views.viewSnowFall.restartFalling()
} }

View File

@ -893,7 +893,7 @@ class RoomDetailViewModel @AssistedInject constructor(
if (sendChatEffect.message.isBlank()) { if (sendChatEffect.message.isBlank()) {
val defaultMessage = stringProvider.getString(when (sendChatEffect.chatEffect) { val defaultMessage = stringProvider.getString(when (sendChatEffect.chatEffect) {
ChatEffect.CONFETTI -> R.string.default_message_emote_confetti ChatEffect.CONFETTI -> R.string.default_message_emote_confetti
ChatEffect.SNOW -> R.string.default_message_emote_snow ChatEffect.SNOWFALL -> R.string.default_message_emote_snow
}) })
room.sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE) room.sendTextMessage(defaultMessage, MessageType.MSGTYPE_EMOTE)
} else { } else {

View File

@ -30,4 +30,5 @@ sealed class RoomListAction : VectorViewModelAction {
data class ToggleTag(val roomId: String, val tag: String) : RoomListAction() data class ToggleTag(val roomId: String, val tag: String) : RoomListAction()
data class LeaveRoom(val roomId: String) : RoomListAction() data class LeaveRoom(val roomId: String) : RoomListAction()
data class JoinSuggestedRoom(val roomId: String, val viaServers: List<String>?) : RoomListAction() data class JoinSuggestedRoom(val roomId: String, val viaServers: List<String>?) : RoomListAction()
data class ShowRoomDetails(val roomId: String, val viaServers: List<String>?) : RoomListAction()
} }

View File

@ -108,10 +108,11 @@ class RoomListFragment @Inject constructor(
sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
roomListViewModel.observeViewEvents { roomListViewModel.observeViewEvents {
when (it) { when (it) {
is RoomListViewEvents.Loading -> showLoading(it.message) is RoomListViewEvents.Loading -> showLoading(it.message)
is RoomListViewEvents.Failure -> showFailure(it.throwable) is RoomListViewEvents.Failure -> showFailure(it.throwable)
is RoomListViewEvents.SelectRoom -> handleSelectRoom(it) is RoomListViewEvents.SelectRoom -> handleSelectRoom(it)
is RoomListViewEvents.Done -> Unit is RoomListViewEvents.Done -> Unit
is RoomListViewEvents.NavigateToMxToBottomSheet -> handleShowMxToLink(it.link)
}.exhaustive }.exhaustive
} }
@ -155,6 +156,10 @@ class RoomListFragment @Inject constructor(
showErrorInSnackbar(throwable) showErrorInSnackbar(throwable)
} }
private fun handleShowMxToLink(link: String) {
navigator.openMatrixToBottomSheet(requireContext(), link)
}
override fun onDestroyView() { override fun onDestroyView() {
adapterInfosList.onEach { it.contentEpoxyController.removeModelBuildListener(modelBuildListener) } adapterInfosList.onEach { it.contentEpoxyController.removeModelBuildListener(modelBuildListener) }
adapterInfosList.clear() adapterInfosList.clear()
@ -474,6 +479,10 @@ class RoomListFragment @Inject constructor(
roomListViewModel.handle(RoomListAction.JoinSuggestedRoom(room.childRoomId, room.viaServers)) roomListViewModel.handle(RoomListAction.JoinSuggestedRoom(room.childRoomId, room.viaServers))
} }
override fun onSuggestedRoomClicked(room: SpaceChildInfo) {
roomListViewModel.handle(RoomListAction.ShowRoomDetails(room.childRoomId, room.viaServers))
}
override fun onRejectRoomInvitation(room: RoomSummary) { override fun onRejectRoomInvitation(room: RoomSummary) {
notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId) notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId)
roomListViewModel.handle(RoomListAction.RejectInvitation(room)) roomListViewModel.handle(RoomListAction.RejectInvitation(room))

View File

@ -26,4 +26,5 @@ interface RoomListListener : FilteredRoomFooterItem.FilteredRoomFooterItemListen
fun onRejectRoomInvitation(room: RoomSummary) fun onRejectRoomInvitation(room: RoomSummary)
fun onAcceptRoomInvitation(room: RoomSummary) fun onAcceptRoomInvitation(room: RoomSummary)
fun onJoinSuggestedRoom(room: SpaceChildInfo) fun onJoinSuggestedRoom(room: SpaceChildInfo)
fun onSuggestedRoomClicked(room: SpaceChildInfo)
} }

View File

@ -29,4 +29,5 @@ sealed class RoomListViewEvents : VectorViewEvents {
data class SelectRoom(val roomSummary: RoomSummary) : RoomListViewEvents() data class SelectRoom(val roomSummary: RoomSummary) : RoomListViewEvents()
object Done : RoomListViewEvents() object Done : RoomListViewEvents()
data class NavigateToMxToBottomSheet(val link: String) : RoomListViewEvents()
} }

View File

@ -161,6 +161,7 @@ class RoomListViewModel @Inject constructor(
is RoomListAction.ToggleTag -> handleToggleTag(action) is RoomListAction.ToggleTag -> handleToggleTag(action)
is RoomListAction.ToggleSection -> handleToggleSection(action.section) is RoomListAction.ToggleSection -> handleToggleSection(action.section)
is RoomListAction.JoinSuggestedRoom -> handleJoinSuggestedRoom(action) is RoomListAction.JoinSuggestedRoom -> handleJoinSuggestedRoom(action)
is RoomListAction.ShowRoomDetails -> handleShowRoomDetails(action)
}.exhaustive }.exhaustive
} }
@ -289,6 +290,12 @@ class RoomListViewModel @Inject constructor(
} }
} }
private fun handleShowRoomDetails(action: RoomListAction.ShowRoomDetails) {
session.permalinkService().createRoomPermalink(action.roomId, action.viaServers)?.let {
_viewEvents.post(RoomListViewEvents.NavigateToMxToBottomSheet(it))
}
}
private fun handleToggleTag(action: RoomListAction.ToggleTag) { private fun handleToggleTag(action: RoomListAction.ToggleTag) {
session.getRoom(action.roomId)?.let { room -> session.getRoom(action.roomId)?.let { room ->
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {

View File

@ -16,7 +16,6 @@
package im.vector.app.features.home.room.list package im.vector.app.features.home.room.list
import android.view.View
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import im.vector.app.R import im.vector.app.R
@ -56,7 +55,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
fun createSuggestion(spaceChildInfo: SpaceChildInfo, fun createSuggestion(spaceChildInfo: SpaceChildInfo,
suggestedRoomJoiningStates: Map<String, Async<Unit>>, suggestedRoomJoiningStates: Map<String, Async<Unit>>,
onJoinClick: View.OnClickListener): VectorEpoxyModel<*> { listener: RoomListListener?): VectorEpoxyModel<*> {
return SpaceChildInfoItem_() return SpaceChildInfoItem_()
.id("sug_${spaceChildInfo.childRoomId}") .id("sug_${spaceChildInfo.childRoomId}")
.matrixItem(spaceChildInfo.toMatrixItem()) .matrixItem(spaceChildInfo.toMatrixItem())
@ -65,7 +64,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
.buttonLabel(stringProvider.getString(R.string.join)) .buttonLabel(stringProvider.getString(R.string.join))
.loading(suggestedRoomJoiningStates[spaceChildInfo.childRoomId] is Loading) .loading(suggestedRoomJoiningStates[spaceChildInfo.childRoomId] is Loading)
.memberCount(spaceChildInfo.activeMemberCount ?: 0) .memberCount(spaceChildInfo.activeMemberCount ?: 0)
.buttonClickListener(onJoinClick) .buttonClickListener(DebouncedClickListener({ listener?.onJoinSuggestedRoom(spaceChildInfo) }))
.itemClickListener(DebouncedClickListener({ listener?.onSuggestedRoomClicked(spaceChildInfo) }))
} }
private fun createInvitationItem(roomSummary: RoomSummary, private fun createInvitationItem(roomSummary: RoomSummary,

View File

@ -48,7 +48,6 @@ abstract class SpaceChildInfoItem : VectorEpoxyModel<SpaceChildInfoItem.Holder>(
@EpoxyAttribute var memberCount: Int = 0 @EpoxyAttribute var memberCount: Int = 0
@EpoxyAttribute var loading: Boolean = false @EpoxyAttribute var loading: Boolean = false
@EpoxyAttribute var space: Boolean = false
@EpoxyAttribute var buttonLabel: String? = null @EpoxyAttribute var buttonLabel: String? = null
@ -63,12 +62,8 @@ abstract class SpaceChildInfoItem : VectorEpoxyModel<SpaceChildInfoItem.Holder>(
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
itemLongClickListener?.onLongClick(it) ?: false itemLongClickListener?.onLongClick(it) ?: false
} }
holder.titleView.text = matrixItem.getBestName() holder.titleView.text = matrixItem.displayName ?: holder.rootView.context.getString(R.string.unnamed_room)
if (space) { avatarRenderer.render(matrixItem, holder.avatarImageView)
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView)
} else {
avatarRenderer.render(matrixItem, holder.avatarImageView)
}
holder.descriptionText.text = span { holder.descriptionText.text = span {
span { span {

View File

@ -24,11 +24,7 @@ class SuggestedRoomListController(
override fun buildModels(data: SuggestedRoomInfo?) { override fun buildModels(data: SuggestedRoomInfo?) {
data?.rooms?.forEach { info -> data?.rooms?.forEach { info ->
roomSummaryItemFactory.createSuggestion(info, data.joinEcho) { add(roomSummaryItemFactory.createSuggestion(info, data.joinEcho, listener))
listener?.onJoinSuggestedRoom(info)
}.let {
add(it)
}
} }
} }
} }

View File

@ -41,14 +41,15 @@ data class MatrixToBottomSheetState(
sealed class RoomInfoResult { sealed class RoomInfoResult {
data class FullInfo( data class FullInfo(
val roomItem: MatrixItem.RoomItem, val roomItem: MatrixItem,
val name: String, val name: String,
val topic: String, val topic: String,
val memberCount: Int?, val memberCount: Int?,
val alias: String?, val alias: String?,
val membership: Membership, val membership: Membership,
val roomType: String?, val roomType: String?,
val viaServers: List<String>? val viaServers: List<String>?,
val isPublic: Boolean
) : RoomInfoResult() ) : RoomInfoResult()
data class PartialInfo( data class PartialInfo(

View File

@ -118,11 +118,9 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
session.getRoom(permalinkData.roomIdOrAlias) session.getRoom(permalinkData.roomIdOrAlias)
} }
?.roomSummary() ?.roomSummary()
// don't take if not active, as it could be outdated // don't take if not Join, as it could be outdated
?.takeIf { it.membership.isActive() } ?.takeIf { it.membership == Membership.JOIN }
// XXX fix that if (knownRoom != null) {
val forceRefresh = true
if (!forceRefresh && knownRoom != null) {
setState { setState {
copy( copy(
roomPeekResult = Success( roomPeekResult = Success(
@ -134,7 +132,8 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
alias = knownRoom.canonicalAlias, alias = knownRoom.canonicalAlias,
membership = knownRoom.membership, membership = knownRoom.membership,
roomType = knownRoom.roomType, roomType = knownRoom.roomType,
viaServers = null viaServers = null,
isPublic = knownRoom.isPublic
) )
) )
) )
@ -150,7 +149,8 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
alias = peekResult.alias, alias = peekResult.alias,
membership = knownRoom?.membership ?: Membership.NONE, membership = knownRoom?.membership ?: Membership.NONE,
roomType = peekResult.roomType, roomType = peekResult.roomType,
viaServers = peekResult.viaServers.takeIf { it.isNotEmpty() } ?: permalinkData.viaParameters viaServers = peekResult.viaServers.takeIf { it.isNotEmpty() } ?: permalinkData.viaParameters,
isPublic = peekResult.isPublic
).also { ).also {
peekResult.someMembers?.let { checkForKnownMembers(it) } peekResult.someMembers?.let { checkForKnownMembers(it) }
} }

View File

@ -39,7 +39,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomType
import javax.inject.Inject import javax.inject.Inject
class MatrixToRoomSpaceFragment @Inject constructor( class MatrixToRoomSpaceFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer private val avatarRenderer: AvatarRenderer,
private val spaceCardRenderer: SpaceCardRenderer
) : VectorBaseFragment<FragmentMatrixToRoomSpaceCardBinding>() { ) : VectorBaseFragment<FragmentMatrixToRoomSpaceCardBinding>() {
private val sharedViewModel: MatrixToBottomSheetViewModel by parentFragmentViewModel() private val sharedViewModel: MatrixToBottomSheetViewModel by parentFragmentViewModel()
@ -78,12 +79,19 @@ class MatrixToRoomSpaceFragment @Inject constructor(
when (val peek = item.invoke()) { when (val peek = item.invoke()) {
is RoomInfoResult.FullInfo -> { is RoomInfoResult.FullInfo -> {
val matrixItem = peek.roomItem val matrixItem = peek.roomItem
avatarRenderer.render(matrixItem, views.matrixToCardAvatar)
if (peek.roomType == RoomType.SPACE) { if (peek.roomType == RoomType.SPACE) {
views.matrixToBetaTag.isVisible = true views.matrixToBetaTag.isVisible = true
avatarRenderer.renderSpace(matrixItem, views.matrixToCardAvatar) views.matrixToAccessImage.isVisible = true
if (peek.isPublic) {
views.matrixToAccessText.setTextOrHide(context?.getString(R.string.public_space))
views.matrixToAccessImage.setImageResource(R.drawable.ic_public_room)
} else {
views.matrixToAccessText.setTextOrHide(context?.getString(R.string.private_space))
views.matrixToAccessImage.setImageResource(R.drawable.ic_room_private)
}
} else { } else {
views.matrixToBetaTag.isVisible = false views.matrixToBetaTag.isVisible = false
avatarRenderer.render(matrixItem, views.matrixToCardAvatar)
} }
views.matrixToCardNameText.setTextOrHide(peek.name) views.matrixToCardNameText.setTextOrHide(peek.name)
views.matrixToCardAliasText.setTextOrHide(peek.alias) views.matrixToCardAliasText.setTextOrHide(peek.alias)
@ -166,25 +174,12 @@ class MatrixToRoomSpaceFragment @Inject constructor(
} }
} }
val images = listOf(views.knownMember1, views.knownMember2, views.knownMember3, views.knownMember4, views.knownMember5) listOf(views.knownMember1, views.knownMember2, views.knownMember3, views.knownMember4, views.knownMember5)
.onEach { it.isGone = true } .onEach { it.isGone = true }
when (state.peopleYouKnow) { when (state.peopleYouKnow) {
is Success -> { is Success -> {
val someYouKnow = state.peopleYouKnow.invoke() val someYouKnow = state.peopleYouKnow.invoke()
if (someYouKnow.isEmpty()) { spaceCardRenderer.renderPeopleYouKnow(views, someYouKnow)
views.peopleYouMayKnowText.isVisible = false
} else {
someYouKnow.forEachIndexed { index, item ->
images[index].isVisible = true
avatarRenderer.render(item, images[index])
}
views.peopleYouMayKnowText.setTextOrHide(
resources.getQuantityString(R.plurals.space_people_you_know,
someYouKnow.count(),
someYouKnow.count()
)
)
}
} }
else -> { else -> {
views.peopleYouMayKnowText.isVisible = false views.peopleYouMayKnowText.isVisible = false

View File

@ -0,0 +1,149 @@
/*
* Copyright (c) 2021 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.matrixto
import androidx.core.view.isGone
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.resources.StringProvider
import im.vector.app.databinding.FragmentMatrixToRoomSpaceCardBinding
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.app.features.home.room.detail.timeline.tools.linkify
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class SpaceCardRenderer @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider
) {
fun render(spaceSummary: RoomSummary?,
peopleYouKnow: List<User>,
matrixLinkCallback: TimelineEventController.UrlClickCallback?,
inCard: FragmentMatrixToRoomSpaceCardBinding) {
if (spaceSummary == null) {
inCard.matrixToCardContentVisibility.isVisible = false
inCard.matrixToCardButtonLoading.isVisible = true
} else {
inCard.matrixToCardContentVisibility.isVisible = true
inCard.matrixToCardButtonLoading.isVisible = false
avatarRenderer.render(spaceSummary.toMatrixItem(), inCard.matrixToCardAvatar)
inCard.matrixToCardNameText.text = spaceSummary.name
inCard.matrixToBetaTag.isVisible = true
inCard.matrixToCardAliasText.setTextOrHide(spaceSummary.canonicalAlias)
inCard.matrixToCardDescText.setTextOrHide(spaceSummary.topic.linkify(matrixLinkCallback))
if (spaceSummary.isPublic) {
inCard.matrixToAccessText.setTextOrHide(stringProvider.getString(R.string.public_space))
inCard.matrixToAccessImage.isVisible = true
inCard.matrixToAccessImage.setImageResource(R.drawable.ic_public_room)
} else {
inCard.matrixToAccessText.setTextOrHide(stringProvider.getString(R.string.private_space))
inCard.matrixToAccessImage.isVisible = true
inCard.matrixToAccessImage.setImageResource(R.drawable.ic_room_private)
}
val memberCount = spaceSummary.otherMemberIds.size
if (memberCount != 0) {
inCard.matrixToMemberPills.isVisible = true
inCard.spaceChildMemberCountText.text = stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)
} else {
// hide the pill
inCard.matrixToMemberPills.isVisible = false
}
renderPeopleYouKnow(inCard, peopleYouKnow.map { it.toMatrixItem() })
}
inCard.matrixToCardDescText.movementMethod = createLinkMovementMethod(object : TimelineEventController.UrlClickCallback {
override fun onUrlClicked(url: String, title: String): Boolean {
return false
}
override fun onUrlLongClicked(url: String): Boolean {
// host.callback?.onUrlInTopicLongClicked(url)
return true
}
})
}
fun render(spaceChildInfo: SpaceChildInfo?,
peopleYouKnow: List<User>,
matrixLinkCallback: TimelineEventController.UrlClickCallback?,
inCard: FragmentMatrixToRoomSpaceCardBinding) {
if (spaceChildInfo == null) {
inCard.matrixToCardContentVisibility.isVisible = false
inCard.matrixToCardButtonLoading.isVisible = true
} else {
inCard.matrixToCardContentVisibility.isVisible = true
inCard.matrixToCardButtonLoading.isVisible = false
avatarRenderer.render(spaceChildInfo.toMatrixItem(), inCard.matrixToCardAvatar)
inCard.matrixToCardNameText.setTextOrHide(spaceChildInfo.name)
inCard.matrixToBetaTag.isVisible = true
inCard.matrixToCardAliasText.setTextOrHide(spaceChildInfo.canonicalAlias)
inCard.matrixToCardDescText.setTextOrHide(spaceChildInfo.topic?.linkify(matrixLinkCallback))
if (spaceChildInfo.worldReadable) {
inCard.matrixToAccessText.setTextOrHide(stringProvider.getString(R.string.public_space))
inCard.matrixToAccessImage.isVisible = true
inCard.matrixToAccessImage.setImageResource(R.drawable.ic_public_room)
} else {
inCard.matrixToAccessText.setTextOrHide(stringProvider.getString(R.string.private_space))
inCard.matrixToAccessImage.isVisible = true
inCard.matrixToAccessImage.setImageResource(R.drawable.ic_room_private)
}
val memberCount = spaceChildInfo.activeMemberCount ?: 0
if (memberCount != 0) {
inCard.matrixToMemberPills.isVisible = true
inCard.spaceChildMemberCountText.text = stringProvider.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)
} else {
// hide the pill
inCard.matrixToMemberPills.isVisible = false
}
renderPeopleYouKnow(inCard, peopleYouKnow.map { it.toMatrixItem() })
}
}
fun renderPeopleYouKnow(inCard: FragmentMatrixToRoomSpaceCardBinding, peopleYouKnow: List<MatrixItem.UserItem>) {
val images = listOf(
inCard.knownMember1,
inCard.knownMember2,
inCard.knownMember3,
inCard.knownMember4,
inCard.knownMember5
).onEach { it.isGone = true }
if (peopleYouKnow.isEmpty()) {
inCard.peopleYouMayKnowText.isVisible = false
} else {
peopleYouKnow.forEachIndexed { index, item ->
images[index].isVisible = true
avatarRenderer.render(item, images[index])
}
inCard.peopleYouMayKnowText.setTextOrHide(
stringProvider.getQuantityString(R.plurals.space_people_you_know,
peopleYouKnow.count(),
peopleYouKnow.count()
)
)
}
}
}

View File

@ -245,7 +245,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val resolvedUrl = when (mode) { val resolvedUrl = when (mode) {
Mode.FULL_SIZE, Mode.FULL_SIZE,
Mode.STICKER -> resolveUrl(data) Mode.STICKER -> resolveUrl(data)
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE) Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE)
} }
// Fallback to base url // Fallback to base url
@ -313,7 +313,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
finalHeight = min(maxImageWidth * height / width, maxImageHeight) finalHeight = min(maxImageWidth * height / width, maxImageHeight)
finalWidth = finalHeight * width / height finalWidth = finalHeight * width / height
} }
Mode.STICKER -> { Mode.STICKER -> {
// limit on width // limit on width
val maxWidthDp = min(dimensionConverter.dpToPx(120), maxImageWidth / 2) val maxWidthDp = min(dimensionConverter.dpToPx(120), maxImageWidth / 2)
finalWidth = min(dimensionConverter.dpToPx(width), maxWidthDp) finalWidth = min(dimensionConverter.dpToPx(width), maxWidthDp)

View File

@ -65,6 +65,7 @@ import im.vector.app.features.pin.PinActivity
import im.vector.app.features.pin.PinArgs import im.vector.app.features.pin.PinArgs
import im.vector.app.features.pin.PinMode import im.vector.app.features.pin.PinMode
import im.vector.app.features.roomdirectory.RoomDirectoryActivity import im.vector.app.features.roomdirectory.RoomDirectoryActivity
import im.vector.app.features.roomdirectory.RoomDirectoryData
import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity import im.vector.app.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.app.features.roomdirectory.roompreview.RoomPreviewActivity import im.vector.app.features.roomdirectory.roompreview.RoomPreviewActivity
import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData
@ -86,7 +87,6 @@ import im.vector.app.features.widgets.WidgetArgsBuilder
import im.vector.app.space import im.vector.app.space
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData
import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.api.session.terms.TermsService
import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType import org.matrix.android.sdk.api.session.widgets.model.WidgetType
@ -129,7 +129,7 @@ class DefaultNavigator @Inject constructor(
} }
appStateHandler.setCurrentSpace(spaceId) appStateHandler.setCurrentSpace(spaceId)
when (postSwitchSpaceAction) { when (postSwitchSpaceAction) {
Navigator.PostSwitchSpaceAction.None -> { Navigator.PostSwitchSpaceAction.None -> {
// go back to home if we are showing room details? // go back to home if we are showing room details?
// This is a bit ugly, but the navigator is supposed to know about the activity stack // This is a bit ugly, but the navigator is supposed to know about the activity stack
if (context is RoomDetailActivity) { if (context is RoomDetailActivity) {
@ -139,7 +139,7 @@ class DefaultNavigator @Inject constructor(
Navigator.PostSwitchSpaceAction.OpenAddExistingRooms -> { Navigator.PostSwitchSpaceAction.OpenAddExistingRooms -> {
startActivity(context, SpaceManageActivity.newIntent(context, spaceId, ManageType.AddRooms), false) startActivity(context, SpaceManageActivity.newIntent(context, spaceId, ManageType.AddRooms), false)
} }
is Navigator.PostSwitchSpaceAction.OpenDefaultRoom -> { is Navigator.PostSwitchSpaceAction.OpenDefaultRoom -> {
val args = RoomDetailArgs( val args = RoomDetailArgs(
postSwitchSpaceAction.roomId, postSwitchSpaceAction.roomId,
eventId = null, eventId = null,
@ -278,7 +278,7 @@ class DefaultNavigator @Inject constructor(
val intent = RoomDirectoryActivity.getIntent(context, initialFilter) val intent = RoomDirectoryActivity.getIntent(context, initialFilter)
context.startActivity(intent) context.startActivity(intent)
} }
is RoomGroupingMethod.BySpace -> { is RoomGroupingMethod.BySpace -> {
val selectedSpace = groupingMethod.space() val selectedSpace = groupingMethod.space()
if (selectedSpace == null) { if (selectedSpace == null) {
val intent = RoomDirectoryActivity.getIntent(context, initialFilter) val intent = RoomDirectoryActivity.getIntent(context, initialFilter)
@ -320,7 +320,7 @@ class DefaultNavigator @Inject constructor(
val intent = InviteUsersToRoomActivity.getIntent(context, roomId) val intent = InviteUsersToRoomActivity.getIntent(context, roomId)
context.startActivity(intent) context.startActivity(intent)
} }
is RoomGroupingMethod.BySpace -> { is RoomGroupingMethod.BySpace -> {
if (currentGroupingMethod.spaceSummary != null) { if (currentGroupingMethod.spaceSummary != null) {
// let user decides if he does it from space or room // let user decides if he does it from space or room
(context as? AppCompatActivity)?.supportFragmentManager?.let { fm -> (context as? AppCompatActivity)?.supportFragmentManager?.let { fm ->

View File

@ -26,11 +26,11 @@ import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.login.LoginConfig import im.vector.app.features.login.LoginConfig
import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.AttachmentData
import im.vector.app.features.pin.PinMode import im.vector.app.features.pin.PinMode
import im.vector.app.features.roomdirectory.RoomDirectoryData
import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData import im.vector.app.features.roomdirectory.roompreview.RoomPreviewData
import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.share.SharedData import im.vector.app.features.share.SharedData
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData
import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.api.session.terms.TermsService
import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
@ -44,7 +44,7 @@ interface Navigator {
sealed class PostSwitchSpaceAction { sealed class PostSwitchSpaceAction {
object None : PostSwitchSpaceAction() object None : PostSwitchSpaceAction()
data class OpenDefaultRoom(val roomId: String, val showShareSheet: Boolean) : PostSwitchSpaceAction() data class OpenDefaultRoom(val roomId: String, val showShareSheet: Boolean) : PostSwitchSpaceAction()
object OpenAddExistingRooms: PostSwitchSpaceAction() object OpenAddExistingRooms : PostSwitchSpaceAction()
} }
fun switchToSpace(context: Context, spaceId: String, postSwitchSpaceAction: PostSwitchSpaceAction) fun switchToSpace(context: Context, spaceId: String, postSwitchSpaceAction: PostSwitchSpaceAction)

View File

@ -21,7 +21,6 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
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.roomdirectory.PublicRoom import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData
data class PublicRoomsViewState( data class PublicRoomsViewState(
// The current filter // The current filter

View File

@ -17,7 +17,6 @@
package im.vector.app.features.roomdirectory package im.vector.app.features.roomdirectory
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData
sealed class RoomDirectoryAction : VectorViewModelAction { sealed class RoomDirectoryAction : VectorViewModelAction {
data class SetRoomDirectoryData(val roomDirectoryData: RoomDirectoryData) : RoomDirectoryAction() data class SetRoomDirectoryData(val roomDirectoryData: RoomDirectoryData) : RoomDirectoryAction()

View File

@ -25,6 +25,7 @@ import im.vector.app.R
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.extensions.popBackstack
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.databinding.ActivitySimpleBinding
import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment
@ -58,7 +59,7 @@ class RoomDirectoryActivity : VectorBaseActivity<ActivitySimpleBinding>() {
.observe() .observe()
.subscribe { sharedAction -> .subscribe { sharedAction ->
when (sharedAction) { when (sharedAction) {
is RoomDirectorySharedAction.Back -> onBackPressed() is RoomDirectorySharedAction.Back -> popBackstack()
is RoomDirectorySharedAction.CreateRoom -> { is RoomDirectorySharedAction.CreateRoom -> {
// Transmit the filter to the CreateRoomFragment // Transmit the filter to the CreateRoomFragment
withState(roomDirectoryViewModel) { withState(roomDirectoryViewModel) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2020 The Matrix.org Foundation C.I.C. * Copyright (c) 2021 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,13 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.api.session.room.model.thirdparty package im.vector.app.features.roomdirectory
/** /**
* This class describes a rooms directory server. * This class describes a rooms directory server protocol.
*/ */
data class RoomDirectoryData( data class RoomDirectoryData(
/** /**
* The server name (might be null) * The server name (might be null)
* Set null when the server is the current user's home server. * Set null when the server is the current user's home server.
@ -30,7 +29,12 @@ data class RoomDirectoryData(
/** /**
* The display name (the server description) * The display name (the server description)
*/ */
val displayName: String = DEFAULT_HOME_SERVER_NAME, val displayName: String = MATRIX_PROTOCOL_NAME,
/**
* the avatar url
*/
val avatarUrl: String? = null,
/** /**
* The third party server identifier * The third party server identifier
@ -40,15 +44,10 @@ data class RoomDirectoryData(
/** /**
* Tell if all the federated servers must be included * Tell if all the federated servers must be included
*/ */
val includeAllNetworks: Boolean = false, val includeAllNetworks: Boolean = false
/**
* the avatar url
*/
val avatarUrl: String? = null
) { ) {
companion object { companion object {
const val DEFAULT_HOME_SERVER_NAME = "Matrix" const val MATRIX_PROTOCOL_NAME = "Matrix"
} }
} }

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2021 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
data class RoomDirectoryServer(
val serverName: String,
/**
* True if this is the current user server
*/
val isUserServer: Boolean,
/**
* True if manually added, so it can be removed by the user
*/
val isManuallyAdded: Boolean,
/**
* Supported protocols
* TODO Rename RoomDirectoryData to RoomDirectoryProtocols
*/
val protocols: List<RoomDirectoryData>
)

View File

@ -37,7 +37,6 @@ import org.matrix.android.sdk.api.session.Session
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.model.roomdirectory.PublicRoomsFilter 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.model.roomdirectory.PublicRoomsParams
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import timber.log.Timber import timber.log.Timber
@ -230,9 +229,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(
Timber.w("Try to join an already joining room. Should not happen") Timber.w("Try to join an already joining room. Should not happen")
return@withState return@withState
} }
val viaServers = state.roomDirectoryData.homeServer val viaServers = listOfNotNull(state.roomDirectoryData.homeServer)
?.let { listOf(it) }
.orEmpty()
viewModelScope.launch { viewModelScope.launch {
try { try {
session.joinRoom(action.roomId, viaServers = viaServers) session.joinRoom(action.roomId, viaServers = viaServers)

View File

@ -75,6 +75,7 @@ class CreateRoomController @Inject constructor(
id("topic") id("topic")
enabled(enableFormElement) enabled(enableFormElement)
value(viewState.roomTopic) value(viewState.roomTopic)
singleLine(false)
hint(host.stringProvider.getString(R.string.create_room_topic_hint)) hint(host.stringProvider.getString(R.string.create_room_topic_hint))
onTextChange { text -> onTextChange { text ->

View File

@ -16,10 +16,12 @@
package im.vector.app.features.roomdirectory.picker package im.vector.app.features.roomdirectory.picker
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
@ -43,6 +45,9 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>()
@EpoxyAttribute @EpoxyAttribute
var includeAllNetworks: Boolean = false var includeAllNetworks: Boolean = false
@EpoxyAttribute
var checked: Boolean = false
@EpoxyAttribute @EpoxyAttribute
var globalListener: (() -> Unit)? = null var globalListener: (() -> Unit)? = null
@ -63,6 +68,7 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>()
holder.nameView.text = directoryName holder.nameView.text = directoryName
holder.descriptionView.setTextOrHide(directoryDescription) holder.descriptionView.setTextOrHide(directoryDescription)
holder.checkedView.isVisible = checked
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
@ -71,5 +77,6 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>()
val avatarView by bind<ImageView>(R.id.itemRoomDirectoryAvatar) val avatarView by bind<ImageView>(R.id.itemRoomDirectoryAvatar)
val nameView by bind<TextView>(R.id.itemRoomDirectoryName) val nameView by bind<TextView>(R.id.itemRoomDirectoryName)
val descriptionView by bind<TextView>(R.id.itemRoomDirectoryDescription) val descriptionView by bind<TextView>(R.id.itemRoomDirectoryDescription)
val checkedView by bind<View>(R.id.itemRoomDirectoryChecked)
} }
} }

View File

@ -18,55 +18,110 @@ package im.vector.app.features.roomdirectory.picker
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.resources.StringArrayProvider import im.vector.app.core.resources.StringArrayProvider
import im.vector.app.features.roomdirectory.RoomDirectoryData
import im.vector.app.features.roomdirectory.RoomDirectoryServer
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
import javax.inject.Inject import javax.inject.Inject
class RoomDirectoryListCreator @Inject constructor(private val stringArrayProvider: StringArrayProvider, class RoomDirectoryListCreator @Inject constructor(
private val session: Session) { private val stringArrayProvider: StringArrayProvider,
private val session: Session
) {
fun computeDirectories(thirdPartyProtocolData: Map<String, ThirdPartyProtocol>): List<RoomDirectoryData> { fun computeDirectories(thirdPartyProtocolData: Map<String, ThirdPartyProtocol>,
val result = ArrayList<RoomDirectoryData>() customHomeservers: Set<String>): List<RoomDirectoryServer> {
val result = ArrayList<RoomDirectoryServer>()
val protocols = ArrayList<RoomDirectoryData>()
// Add user homeserver name // Add user homeserver name
val userHsName = session.myUserId.substringAfter(":") val userHsName = session.myUserId.substringAfter(":")
result.add(RoomDirectoryData( // Add default protocol
displayName = userHsName, protocols.add(
includeAllNetworks = true RoomDirectoryData(
)) homeServer = null,
displayName = RoomDirectoryData.MATRIX_PROTOCOL_NAME,
// Add user's HS but for Matrix public rooms only includeAllNetworks = false
result.add(RoomDirectoryData()) )
)
// Add custom directory servers
val hsNamesList = stringArrayProvider.getStringArray(R.array.room_directory_servers)
hsNamesList.forEach {
if (it != userHsName) {
// Use the server name as a default display name
result.add(RoomDirectoryData(
homeServer = it,
displayName = it,
includeAllNetworks = true
))
}
}
// Add result of the request // Add result of the request
thirdPartyProtocolData.forEach { thirdPartyProtocolData.forEach {
it.value.instances?.forEach { thirdPartyProtocolInstance -> it.value.instances?.forEach { thirdPartyProtocolInstance ->
result.add(RoomDirectoryData( protocols.add(
homeServer = null, RoomDirectoryData(
displayName = thirdPartyProtocolInstance.desc ?: "", homeServer = null,
thirdPartyInstanceId = thirdPartyProtocolInstance.instanceId, displayName = thirdPartyProtocolInstance.desc ?: "",
includeAllNetworks = false, thirdPartyInstanceId = thirdPartyProtocolInstance.instanceId,
// Default to protocol icon includeAllNetworks = false,
avatarUrl = thirdPartyProtocolInstance.icon ?: it.value.icon // Default to protocol icon
)) avatarUrl = thirdPartyProtocolInstance.icon ?: it.value.icon
)
)
} }
} }
// Add all rooms
protocols.add(
RoomDirectoryData(
homeServer = null,
displayName = RoomDirectoryData.MATRIX_PROTOCOL_NAME,
includeAllNetworks = true
)
)
result.add(
RoomDirectoryServer(
serverName = userHsName,
isUserServer = true,
isManuallyAdded = false,
protocols = protocols
)
)
// Add custom directory servers, form the config file, excluding the current user homeserver
stringArrayProvider.getStringArray(R.array.room_directory_servers)
.filter { it != userHsName }
.forEach {
// Use the server name as a default display name
result.add(
RoomDirectoryServer(
serverName = it,
isUserServer = false,
isManuallyAdded = false,
protocols = listOf(
RoomDirectoryData(
homeServer = it,
displayName = RoomDirectoryData.MATRIX_PROTOCOL_NAME,
includeAllNetworks = false
)
)
)
)
}
// Add manually added server by the user
customHomeservers
.forEach {
// Use the server name as a default display name
result.add(
RoomDirectoryServer(
serverName = it,
isUserServer = false,
isManuallyAdded = true,
protocols = listOf(
RoomDirectoryData(
homeServer = it,
displayName = RoomDirectoryData.MATRIX_PROTOCOL_NAME,
includeAllNetworks = false
)
)
)
)
}
return result return result
} }
} }

View File

@ -17,7 +17,14 @@
package im.vector.app.features.roomdirectory.picker package im.vector.app.features.roomdirectory.picker
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.roomdirectory.RoomDirectoryServer
sealed class RoomDirectoryPickerAction : VectorViewModelAction { sealed class RoomDirectoryPickerAction : VectorViewModelAction {
object Retry : RoomDirectoryPickerAction() object Retry : RoomDirectoryPickerAction()
object EnterEditMode : RoomDirectoryPickerAction()
object ExitEditMode : RoomDirectoryPickerAction()
data class SetServerUrl(val url: String) : RoomDirectoryPickerAction()
data class RemoveServer(val roomDirectoryServer: RoomDirectoryServer) : RoomDirectoryPickerAction()
object Submit : RoomDirectoryPickerAction()
} }

View File

@ -16,37 +16,62 @@
package im.vector.app.features.roomdirectory.picker package im.vector.app.features.roomdirectory.picker
import android.text.InputType
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.dividerItem
import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.join
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData import im.vector.app.core.ui.list.genericButtonItem
import im.vector.app.core.ui.list.verticalMarginItem
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.discovery.settingsContinueCancelItem
import im.vector.app.features.discovery.settingsInformationItem
import im.vector.app.features.form.formEditTextItem
import im.vector.app.features.roomdirectory.RoomDirectoryData
import im.vector.app.features.roomdirectory.RoomDirectoryServer
import org.matrix.android.sdk.api.failure.Failure
import javax.inject.Inject import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection
class RoomDirectoryPickerController @Inject constructor(private val stringProvider: StringProvider, class RoomDirectoryPickerController @Inject constructor(
private val errorFormatter: ErrorFormatter, private val stringProvider: StringProvider,
private val roomDirectoryListCreator: RoomDirectoryListCreator private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter,
private val errorFormatter: ErrorFormatter
) : TypedEpoxyController<RoomDirectoryPickerViewState>() { ) : TypedEpoxyController<RoomDirectoryPickerViewState>() {
var currentRoomDirectoryData: RoomDirectoryData? = null
var callback: Callback? = null var callback: Callback? = null
var index = 0 private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
override fun buildModels(viewState: RoomDirectoryPickerViewState) { override fun buildModels(data: RoomDirectoryPickerViewState) {
val host = this val host = this
val asyncThirdPartyProtocol = viewState.asyncThirdPartyRequest
when (asyncThirdPartyProtocol) { when (val asyncThirdPartyProtocol = data.asyncThirdPartyRequest) {
is Success -> { is Success -> {
val directories = roomDirectoryListCreator.computeDirectories(asyncThirdPartyProtocol()) data.directories.join(
each = { _, roomDirectoryServer -> buildDirectory(roomDirectoryServer) },
directories.forEach { between = { idx, _ -> buildDivider(idx) }
buildDirectory(it) )
buildForm(data)
verticalMarginItem {
id("space_bottom")
heightInPx(host.dimensionConverter.dpToPx(16))
} }
} }
is Incomplete -> { is Incomplete -> {
@ -64,28 +89,131 @@ class RoomDirectoryPickerController @Inject constructor(private val stringProvid
} }
} }
private fun buildDirectory(roomDirectoryData: RoomDirectoryData) { private fun buildForm(data: RoomDirectoryPickerViewState) {
buildDivider(1000)
val host = this val host = this
roomDirectoryItem { if (data.inEditMode) {
id(host.index++) verticalMarginItem {
id("form_space")
directoryName(roomDirectoryData.displayName) heightInPx(host.dimensionConverter.dpToPx(16))
val description = when {
roomDirectoryData.includeAllNetworks ->
host.stringProvider.getString(R.string.directory_server_all_rooms_on_server, roomDirectoryData.displayName)
"Matrix" == roomDirectoryData.displayName ->
host.stringProvider.getString(R.string.directory_server_native_rooms, roomDirectoryData.displayName)
else ->
null
} }
settingsInformationItem {
id("form_notice")
message(host.stringProvider.getString(R.string.directory_add_a_new_server_prompt))
colorProvider(host.colorProvider)
}
verticalMarginItem {
id("form_space_2")
heightInPx(host.dimensionConverter.dpToPx(8))
}
formEditTextItem {
id("edit")
showBottomSeparator(false)
value(data.enteredServer)
imeOptions(EditorInfo.IME_ACTION_DONE)
editorActionListener(object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId == EditorInfo.IME_ACTION_DONE) {
if (data.enteredServer.isNotEmpty()) {
host.callback?.onSubmitServer()
}
return true
}
return false
}
})
hint(host.stringProvider.getString(R.string.directory_server_placeholder))
inputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
onTextChange { text ->
host.callback?.onEnterServerChange(text)
}
when (data.addServerAsync) {
Uninitialized -> enabled(true)
is Loading -> enabled(false)
is Success -> enabled(false)
is Fail -> {
enabled(true)
errorMessage(host.getErrorMessage(data.addServerAsync.error))
}
}
}
when (data.addServerAsync) {
Uninitialized,
is Fail -> settingsContinueCancelItem {
id("continueCancel")
continueText(host.stringProvider.getString(R.string.ok))
canContinue(data.enteredServer.isNotEmpty())
continueOnClick { host.callback?.onSubmitServer() }
cancelOnClick { host.callback?.onCancelEnterServer() }
}
is Loading -> loadingItem {
id("addLoading")
}
is Success -> Unit /* This is a transitive state */
}
} else {
genericButtonItem {
id("add")
text(host.stringProvider.getString(R.string.directory_add_a_new_server))
textColor(host.colorProvider.getColor(R.color.riotx_accent))
buttonClickAction(View.OnClickListener {
host.callback?.onStartEnterServer()
})
}
}
}
directoryDescription(description) private fun getErrorMessage(error: Throwable): String {
directoryAvatarUrl(roomDirectoryData.avatarUrl) return if (error is Failure.ServerError
includeAllNetworks(roomDirectoryData.includeAllNetworks) && error.httpCode == HttpsURLConnection.HTTP_INTERNAL_ERROR /* 500 */) {
stringProvider.getString(R.string.directory_add_a_new_server_error)
} else {
errorFormatter.toHumanReadable(error)
}
}
globalListener { private fun buildDivider(idx: Int) {
host.callback?.onRoomDirectoryClicked(roomDirectoryData) val host = this
dividerItem {
id("divider_$idx")
color(host.dividerColor)
}
}
private fun buildDirectory(roomDirectoryServer: RoomDirectoryServer) {
val host = this
roomDirectoryServerItem {
id("server_$roomDirectoryServer")
serverName(roomDirectoryServer.serverName)
canRemove(roomDirectoryServer.isManuallyAdded)
removeListener { host.callback?.onRemoveServer(roomDirectoryServer) }
if (roomDirectoryServer.isUserServer) {
serverDescription(host.stringProvider.getString(R.string.directory_your_server))
}
}
roomDirectoryServer.protocols.forEach { roomDirectoryData ->
roomDirectoryItem {
id("server_${roomDirectoryServer}_proto_$roomDirectoryData")
directoryName(
if (roomDirectoryData.includeAllNetworks) {
host.stringProvider.getString(R.string.directory_server_all_rooms_on_server, roomDirectoryServer.serverName)
} else {
roomDirectoryData.displayName
}
)
if (roomDirectoryData.displayName == RoomDirectoryData.MATRIX_PROTOCOL_NAME && !roomDirectoryData.includeAllNetworks) {
directoryDescription(
host.stringProvider.getString(R.string.directory_server_native_rooms, roomDirectoryServer.serverName)
)
}
directoryAvatarUrl(roomDirectoryData.avatarUrl)
includeAllNetworks(roomDirectoryData.includeAllNetworks)
checked(roomDirectoryData == host.currentRoomDirectoryData)
globalListener {
host.callback?.onRoomDirectoryClicked(roomDirectoryData)
}
} }
} }
} }
@ -93,5 +221,10 @@ class RoomDirectoryPickerController @Inject constructor(private val stringProvid
interface Callback { interface Callback {
fun onRoomDirectoryClicked(roomDirectoryData: RoomDirectoryData) fun onRoomDirectoryClicked(roomDirectoryData: RoomDirectoryData)
fun retry() fun retry()
fun onStartEnterServer()
fun onEnterServerChange(server: String)
fun onSubmitServer()
fun onCancelEnterServer()
fun onRemoveServer(roomDirectoryServer: RoomDirectoryServer)
} }
} }

View File

@ -18,7 +18,6 @@ package im.vector.app.features.roomdirectory.picker
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@ -28,21 +27,22 @@ import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding
import im.vector.app.features.roomdirectory.RoomDirectoryAction import im.vector.app.features.roomdirectory.RoomDirectoryAction
import im.vector.app.features.roomdirectory.RoomDirectoryData
import im.vector.app.features.roomdirectory.RoomDirectoryServer
import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedAction
import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel
import im.vector.app.features.roomdirectory.RoomDirectoryViewModel import im.vector.app.features.roomdirectory.RoomDirectoryViewModel
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// TODO Menu to add custom room directory (not done in RiotWeb so far...)
class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerViewModelFactory: RoomDirectoryPickerViewModel.Factory, class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerViewModelFactory: RoomDirectoryPickerViewModel.Factory,
private val roomDirectoryPickerController: RoomDirectoryPickerController private val roomDirectoryPickerController: RoomDirectoryPickerController
) : VectorBaseFragment<FragmentRoomDirectoryPickerBinding>(), ) : VectorBaseFragment<FragmentRoomDirectoryPickerBinding>(),
OnBackPressed,
RoomDirectoryPickerController.Callback { RoomDirectoryPickerController.Callback {
private val viewModel: RoomDirectoryViewModel by activityViewModel() private val viewModel: RoomDirectoryViewModel by activityViewModel()
@ -65,6 +65,11 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java)
setupRecyclerView() setupRecyclerView()
// Give the current data to our controller. There maybe a better way to do that...
withState(viewModel) {
roomDirectoryPickerController.currentRoomDirectoryData = it.roomDirectoryData
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -73,18 +78,6 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
super.onDestroyView() super.onDestroyView()
} }
override fun getMenuRes() = R.menu.menu_directory_server_picker
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.action_add_custom_hs) {
// TODO
vectorBaseActivity.notImplemented("Entering custom homeserver")
return true
}
return super.onOptionsItemSelected(item)
}
private fun setupRecyclerView() { private fun setupRecyclerView() {
views.roomDirectoryPickerList.configureWith(roomDirectoryPickerController) views.roomDirectoryPickerList.configureWith(roomDirectoryPickerController)
roomDirectoryPickerController.callback = this roomDirectoryPickerController.callback = this
@ -97,6 +90,26 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
sharedActionViewModel.post(RoomDirectorySharedAction.Back) sharedActionViewModel.post(RoomDirectorySharedAction.Back)
} }
override fun onStartEnterServer() {
pickerViewModel.handle(RoomDirectoryPickerAction.EnterEditMode)
}
override fun onCancelEnterServer() {
pickerViewModel.handle(RoomDirectoryPickerAction.ExitEditMode)
}
override fun onEnterServerChange(server: String) {
pickerViewModel.handle(RoomDirectoryPickerAction.SetServerUrl(server))
}
override fun onSubmitServer() {
pickerViewModel.handle(RoomDirectoryPickerAction.Submit)
}
override fun onRemoveServer(roomDirectoryServer: RoomDirectoryServer) {
pickerViewModel.handle(RoomDirectoryPickerAction.RemoveServer(roomDirectoryServer))
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.select_room_directory) (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.select_room_directory)
@ -111,4 +124,16 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
// Populate list with Epoxy // Populate list with Epoxy
roomDirectoryPickerController.setData(state) roomDirectoryPickerController.setData(state)
} }
override fun onBackPressed(toolbarButton: Boolean): Boolean {
// Leave the add server mode if started
return withState(pickerViewModel) {
if (it.inEditMode) {
pickerViewModel.handle(RoomDirectoryPickerAction.ExitEditMode)
true
} else {
false
}
}
}
} }

View File

@ -22,18 +22,28 @@ import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
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.core.resources.StringProvider
import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initialState: RoomDirectoryPickerViewState, class RoomDirectoryPickerViewModel @AssistedInject constructor(
private val session: Session) @Assisted initialState: RoomDirectoryPickerViewState,
: VectorViewModel<RoomDirectoryPickerViewState, RoomDirectoryPickerAction, EmptyViewEvents>(initialState) { private val session: Session,
private val uiStateRepository: UiStateRepository,
private val stringProvider: StringProvider,
private val roomDirectoryListCreator: RoomDirectoryListCreator
) : VectorViewModel<RoomDirectoryPickerViewState, RoomDirectoryPickerAction, EmptyViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -50,7 +60,22 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initial
} }
init { init {
observeAndCompute()
load() load()
loadCustomRoomDirectoryHomeservers()
}
private fun observeAndCompute() {
selectSubscribe(
RoomDirectoryPickerViewState::asyncThirdPartyRequest,
RoomDirectoryPickerViewState::customHomeservers
) { async, custom ->
async()?.let {
setState {
copy(directories = roomDirectoryListCreator.computeDirectories(it, custom))
}
}
}
} }
private fun load() { private fun load() {
@ -71,9 +96,101 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initial
} }
} }
private fun loadCustomRoomDirectoryHomeservers() {
setState {
copy(
customHomeservers = uiStateRepository.getCustomRoomDirectoryHomeservers(session.sessionId)
)
}
}
override fun handle(action: RoomDirectoryPickerAction) { override fun handle(action: RoomDirectoryPickerAction) {
when (action) { when (action) {
RoomDirectoryPickerAction.Retry -> load() RoomDirectoryPickerAction.Retry -> load()
RoomDirectoryPickerAction.EnterEditMode -> handleEnterEditMode()
RoomDirectoryPickerAction.ExitEditMode -> handleExitEditMode()
is RoomDirectoryPickerAction.SetServerUrl -> handleSetServerUrl(action)
RoomDirectoryPickerAction.Submit -> handleSubmit()
is RoomDirectoryPickerAction.RemoveServer -> handleRemoveServer(action)
}.exhaustive
}
private fun handleEnterEditMode() {
setState {
copy(
inEditMode = true,
enteredServer = "",
addServerAsync = Uninitialized
)
}
}
private fun handleExitEditMode() {
setState {
copy(
inEditMode = false,
enteredServer = "",
addServerAsync = Uninitialized
)
}
}
private fun handleSetServerUrl(action: RoomDirectoryPickerAction.SetServerUrl) {
setState {
copy(
enteredServer = action.url
)
}
}
private fun handleSubmit() = withState { state ->
// First avoid duplicate
val enteredServer = state.enteredServer
val existingServerList = state.directories.map { it.serverName }
if (enteredServer in existingServerList) {
setState {
copy(addServerAsync = Fail(Throwable(stringProvider.getString(R.string.directory_add_a_new_server_error_already_added))))
}
return@withState
}
viewModelScope.launch {
setState {
copy(addServerAsync = Loading())
}
try {
session.getPublicRooms(
server = enteredServer,
publicRoomsParams = PublicRoomsParams(limit = 1)
)
// Success, let add the server to our local repository, and update the state
val newSet = uiStateRepository.getCustomRoomDirectoryHomeservers(session.sessionId) + enteredServer
uiStateRepository.setCustomRoomDirectoryHomeservers(session.sessionId, newSet)
setState {
copy(
inEditMode = false,
enteredServer = "",
addServerAsync = Uninitialized,
customHomeservers = newSet
)
}
} catch (failure: Throwable) {
setState {
copy(addServerAsync = Fail(failure))
}
}
}
}
private fun handleRemoveServer(action: RoomDirectoryPickerAction.RemoveServer) {
val newSet = uiStateRepository.getCustomRoomDirectoryHomeservers(session.sessionId) - action.roomDirectoryServer.serverName
uiStateRepository.setCustomRoomDirectoryHomeservers(session.sessionId, newSet)
setState {
copy(
customHomeservers = newSet
)
} }
} }
} }

View File

@ -19,8 +19,15 @@ package im.vector.app.features.roomdirectory.picker
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.roomdirectory.RoomDirectoryServer
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
data class RoomDirectoryPickerViewState( data class RoomDirectoryPickerViewState(
val asyncThirdPartyRequest: Async<Map<String, ThirdPartyProtocol>> = Uninitialized val asyncThirdPartyRequest: Async<Map<String, ThirdPartyProtocol>> = Uninitialized,
val customHomeservers: Set<String> = emptySet(),
val inEditMode: Boolean = false,
val enteredServer: String = "",
val addServerAsync: Async<Unit> = Uninitialized,
// computed
val directories: List<RoomDirectoryServer> = emptyList()
) : MvRxState ) : MvRxState

View File

@ -0,0 +1,59 @@
/*
* Copyright 2021 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.picker
import android.view.View
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_room_directory_server)
abstract class RoomDirectoryServerItem : VectorEpoxyModel<RoomDirectoryServerItem.Holder>() {
@EpoxyAttribute
var serverName: String? = null
@EpoxyAttribute
var serverDescription: String? = null
@EpoxyAttribute
var canRemove: Boolean = false
@EpoxyAttribute
var removeListener: ClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.nameView.text = serverName
holder.descriptionView.setTextOrHide(serverDescription)
holder.deleteView.isVisible = canRemove
holder.deleteView.onClick(removeListener)
}
class Holder : VectorEpoxyHolder() {
val nameView by bind<TextView>(R.id.itemRoomDirectoryServerName)
val descriptionView by bind<TextView>(R.id.itemRoomDirectoryServerDescription)
val deleteView by bind<View>(R.id.itemRoomDirectoryServerRemove)
}
}

View File

@ -25,9 +25,9 @@ import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.databinding.ActivitySimpleBinding
import im.vector.app.features.roomdirectory.RoomDirectoryData
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import timber.log.Timber import timber.log.Timber

View File

@ -68,16 +68,14 @@ class RoomSettingsController @Inject constructor(
id("avatar") id("avatar")
enabled(data.actionPermissions.canChangeAvatar) enabled(data.actionPermissions.canChangeAvatar)
when (val avatarAction = data.avatarAction) { when (val avatarAction = data.avatarAction) {
RoomSettingsViewState.AvatarAction.None -> { RoomSettingsViewState.AvatarAction.None -> {
// Use the current value // Use the current value
avatarRenderer(host.avatarRenderer) avatarRenderer(host.avatarRenderer)
// We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. // We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar.
matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl)) matrixItem(roomSummary.toMatrixItem().updateAvatar(data.currentRoomAvatarUrl))
} }
RoomSettingsViewState.AvatarAction.DeleteAvatar -> RoomSettingsViewState.AvatarAction.DeleteAvatar -> imageUri(null)
imageUri(null) is RoomSettingsViewState.AvatarAction.UpdateAvatar -> imageUri(avatarAction.newAvatarUri)
is RoomSettingsViewState.AvatarAction.UpdateAvatar ->
imageUri(avatarAction.newAvatarUri)
} }
clickListener { host.callback?.onAvatarChange() } clickListener { host.callback?.onAvatarChange() }
deleteListener { host.callback?.onAvatarDelete() } deleteListener { host.callback?.onAvatarDelete() }
@ -102,6 +100,7 @@ class RoomSettingsController @Inject constructor(
id("topic") id("topic")
enabled(data.actionPermissions.canChangeTopic) enabled(data.actionPermissions.canChangeTopic)
value(data.newTopic ?: roomSummary.topic) value(data.newTopic ?: roomSummary.topic)
singleLine(false)
hint(host.stringProvider.getString(R.string.room_settings_topic_hint)) hint(host.stringProvider.getString(R.string.room_settings_topic_hint))
onTextChange { text -> onTextChange { text ->

View File

@ -96,7 +96,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
val session = activeSessionHolder.getSafeActiveSession() ?: return val session = activeSessionHolder.getSafeActiveSession() ?: return
val roomSummary = session.getRoomSummary(spaceArgs.spaceId) val roomSummary = session.getRoomSummary(spaceArgs.spaceId)
roomSummary?.toMatrixItem()?.let { roomSummary?.toMatrixItem()?.let {
avatarRenderer.renderSpace(it, views.spaceAvatarImageView) avatarRenderer.render(it, views.spaceAvatarImageView)
} }
views.spaceNameView.text = roomSummary?.displayName views.spaceNameView.text = roomSummary?.displayName
views.spaceDescription.setTextOrHide(roomSummary?.topic?.takeIf { it.isNotEmpty() }) views.spaceDescription.setTextOrHide(roomSummary?.topic?.takeIf { it.isNotEmpty() })

View File

@ -87,7 +87,7 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
holder.indentSpace.isVisible = indent > 0 holder.indentSpace.isVisible = indent > 0
holder.separator.isVisible = showSeparator holder.separator.isVisible = showSeparator
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.counterBadgeView.render(countState) holder.counterBadgeView.render(countState)
} }

View File

@ -81,7 +81,7 @@ abstract class SubSpaceSummaryItem : VectorEpoxyModel<SubSpaceSummaryItem.Holder
width = indent * 30 width = indent * 30
} }
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.counterBadgeView.render(countState) holder.counterBadgeView.render(countState)
} }

View File

@ -69,7 +69,6 @@ class SpaceDefaultRoomEpoxyController @Inject constructor(
id("roomName1") id("roomName1")
enabled(true) enabled(true)
value(firstRoomName) value(firstRoomName)
singleLine(true)
hint(host.stringProvider.getString(R.string.create_room_name_section)) hint(host.stringProvider.getString(R.string.create_room_name_section))
endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT) endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT)
showBottomSeparator(false) showBottomSeparator(false)
@ -83,7 +82,6 @@ class SpaceDefaultRoomEpoxyController @Inject constructor(
id("roomName2") id("roomName2")
enabled(true) enabled(true)
value(secondRoomName) value(secondRoomName)
singleLine(true)
hint(host.stringProvider.getString(R.string.create_room_name_section)) hint(host.stringProvider.getString(R.string.create_room_name_section))
endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT) endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT)
showBottomSeparator(false) showBottomSeparator(false)
@ -97,7 +95,6 @@ class SpaceDefaultRoomEpoxyController @Inject constructor(
id("roomName3") id("roomName3")
enabled(true) enabled(true)
value(thirdRoomName) value(thirdRoomName)
singleLine(true)
hint(host.stringProvider.getString(R.string.create_room_name_section)) hint(host.stringProvider.getString(R.string.create_room_name_section))
endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT) endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT)
showBottomSeparator(false) showBottomSeparator(false)

View File

@ -54,7 +54,7 @@ class SpaceDetailEpoxyController @Inject constructor(
enabled(true) enabled(true)
imageUri(data?.avatarUri) imageUri(data?.avatarUri)
avatarRenderer(host.avatarRenderer) avatarRenderer(host.avatarRenderer)
matrixItem(data?.name?.let { MatrixItem.RoomItem("!", it, null).takeIf { !it.displayName.isNullOrBlank() } }) matrixItem(data?.name?.let { MatrixItem.SpaceItem("!", it, null).takeIf { !it.displayName.isNullOrBlank() } })
clickListener { host.listener?.onAvatarChange() } clickListener { host.listener?.onAvatarChange() }
deleteListener { host.listener?.onAvatarDelete() } deleteListener { host.listener?.onAvatarDelete() }
} }
@ -64,7 +64,6 @@ class SpaceDetailEpoxyController @Inject constructor(
enabled(true) enabled(true)
value(data?.name) value(data?.name)
hint(host.stringProvider.getString(R.string.create_room_name_hint)) hint(host.stringProvider.getString(R.string.create_room_name_hint))
singleLine(true)
showBottomSeparator(false) showBottomSeparator(false)
errorMessage(data?.nameInlineError) errorMessage(data?.nameInlineError)
// onBind { _, view, _ -> // onBind { _, view, _ ->

View File

@ -122,13 +122,15 @@ class SpaceDirectoryController @Inject constructor(
val isSpace = info.roomType == RoomType.SPACE val isSpace = info.roomType == RoomType.SPACE
val isJoined = data?.joinedRoomsIds?.contains(info.childRoomId) == true val isJoined = data?.joinedRoomsIds?.contains(info.childRoomId) == true
val isLoading = data?.changeMembershipStates?.get(info.childRoomId)?.isInProgress() ?: false val isLoading = data?.changeMembershipStates?.get(info.childRoomId)?.isInProgress() ?: false
// if it's known use that matrixItem because it would have a better computed name
val matrixItem = data?.knownRoomSummaries?.find { it.roomId == info.childRoomId }?.toMatrixItem()
?: info.toMatrixItem()
spaceChildInfoItem { spaceChildInfoItem {
id(info.childRoomId) id(info.childRoomId)
matrixItem(info.toMatrixItem()) matrixItem(matrixItem)
avatarRenderer(host.avatarRenderer) avatarRenderer(host.avatarRenderer)
topic(info.topic) topic(info.topic)
memberCount(info.activeMemberCount ?: 0) memberCount(info.activeMemberCount ?: 0)
space(isSpace)
loading(isLoading) loading(isLoading)
buttonLabel( buttonLabel(
if (isJoined) host.stringProvider.getString(R.string.action_open) if (isJoined) host.stringProvider.getString(R.string.action_open)

View File

@ -16,6 +16,7 @@
package im.vector.app.features.spaces.explore package im.vector.app.features.spaces.explore
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
@ -23,19 +24,33 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.text.toSpannable
import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.withColoredButton
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.colorizeMatchingText
import im.vector.app.core.utils.isValidUrl
import im.vector.app.core.utils.openUrlInExternalBrowser
import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.matrixto.SpaceCardRenderer
import im.vector.app.features.permalink.PermalinkHandler
import im.vector.app.features.spaces.manage.ManageType import im.vector.app.features.spaces.manage.ManageType
import im.vector.app.features.spaces.manage.SpaceManageActivity import im.vector.app.features.spaces.manage.SpaceManageActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import java.net.URL
import javax.inject.Inject import javax.inject.Inject
@Parcelize @Parcelize
@ -44,9 +59,13 @@ data class SpaceDirectoryArgs(
) : Parcelable ) : Parcelable
class SpaceDirectoryFragment @Inject constructor( class SpaceDirectoryFragment @Inject constructor(
private val epoxyController: SpaceDirectoryController private val epoxyController: SpaceDirectoryController,
private val permalinkHandler: PermalinkHandler,
private val spaceCardRenderer: SpaceCardRenderer,
private val colorProvider: ColorProvider
) : VectorBaseFragment<FragmentRoomDirectoryPickerBinding>(), ) : VectorBaseFragment<FragmentRoomDirectoryPickerBinding>(),
SpaceDirectoryController.InteractionListener, SpaceDirectoryController.InteractionListener,
TimelineEventController.UrlClickCallback,
OnBackPressed { OnBackPressed {
override fun getMenuRes() = R.menu.menu_space_directory override fun getMenuRes() = R.menu.menu_space_directory
@ -71,6 +90,9 @@ class SpaceDirectoryFragment @Inject constructor(
viewModel.selectSubscribe(this, SpaceDirectoryState::canAddRooms) { viewModel.selectSubscribe(this, SpaceDirectoryState::canAddRooms) {
invalidateOptionsMenu() invalidateOptionsMenu()
} }
views.spaceCard.matrixToCardMainButton.isVisible = false
views.spaceCard.matrixToCardSecondaryButton.isVisible = false
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -82,10 +104,21 @@ class SpaceDirectoryFragment @Inject constructor(
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
epoxyController.setData(state) epoxyController.setData(state)
val title = state.hierarchyStack.lastOrNull()?.let { currentParent -> val currentParent = state.hierarchyStack.lastOrNull()?.let { currentParent ->
state.spaceSummaryApiResult.invoke()?.firstOrNull { it.childRoomId == currentParent } state.spaceSummaryApiResult.invoke()?.firstOrNull { it.childRoomId == currentParent }
}?.name ?: getString(R.string.space_explore_activity_title) }
views.toolbar.title = title
if (currentParent == null) {
val title = getString(R.string.space_explore_activity_title)
views.toolbar.title = title
spaceCardRenderer.render(state.spaceSummary.invoke(), emptyList(), this, views.spaceCard)
} else {
val title = currentParent.name ?: currentParent.canonicalAlias ?: getString(R.string.space_explore_activity_title)
views.toolbar.title = title
spaceCardRenderer.render(currentParent, emptyList(), this, views.spaceCard)
}
} }
override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state -> override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state ->
@ -96,7 +129,7 @@ class SpaceDirectoryFragment @Inject constructor(
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.spaceAddRoom -> { R.id.spaceAddRoom -> {
withState(viewModel) { state -> withState(viewModel) { state ->
addExistingRooms(state.spaceId) addExistingRooms(state.spaceId)
} }
@ -138,6 +171,44 @@ class SpaceDirectoryFragment @Inject constructor(
override fun addExistingRooms(spaceId: String) { override fun addExistingRooms(spaceId: String) {
addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRooms)) addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRooms))
} }
override fun onUrlClicked(url: String, title: String): Boolean {
permalinkHandler
.launch(requireActivity(), url, null)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { managed ->
if (!managed) {
if (title.isValidUrl() && url.isValidUrl() && URL(title).host != URL(url).host) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.external_link_confirmation_title)
.setMessage(
getString(R.string.external_link_confirmation_message, title, url)
.toSpannable()
.colorizeMatchingText(url, colorProvider.getColorFromAttribute(R.attr.riotx_text_primary_body_contrast))
.colorizeMatchingText(title, colorProvider.getColorFromAttribute(R.attr.riotx_text_primary_body_contrast))
)
.setPositiveButton(R.string._continue) { _, _ ->
openUrlInExternalBrowser(requireContext(), url)
}
.setNegativeButton(R.string.cancel, null)
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
} else {
// Open in external browser, in a new Tab
openUrlInExternalBrowser(requireContext(), url)
}
}
}
.disposeOnDestroyView()
// In fact it is always managed
return true
}
override fun onUrlLongClicked(url: String): Boolean {
// nothing?
return false
}
// override fun navigateToRoom(roomId: String) { // override fun navigateToRoom(roomId: String) {
// viewModel.handle(SpaceDirectoryViewAction.NavigateToRoom(roomId)) // viewModel.handle(SpaceDirectoryViewAction.NavigateToRoom(roomId))
// } // }

View File

@ -37,7 +37,9 @@ data class SpaceDirectoryState(
val joinedRoomsIds: Set<String> = emptySet(), val joinedRoomsIds: Set<String> = emptySet(),
// keys are room alias or roomId // keys are room alias or roomId
val changeMembershipStates: Map<String, ChangeMembershipState> = emptyMap(), val changeMembershipStates: Map<String, ChangeMembershipState> = emptyMap(),
val canAddRooms: Boolean = false val canAddRooms: Boolean = false,
// cached room summaries of known rooms
val knownRoomSummaries : List<RoomSummary> = emptyList()
) : MvRxState { ) : MvRxState {
constructor(args: SpaceDirectoryArgs) : this( constructor(args: SpaceDirectoryArgs) : this(
spaceId = args.spaceId spaceId = args.spaceId

View File

@ -66,7 +66,8 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
val spaceSum = session.getRoomSummary(initialState.spaceId) val spaceSum = session.getRoomSummary(initialState.spaceId)
setState { setState {
copy( copy(
childList = spaceSum?.spaceChildren ?: emptyList() childList = spaceSum?.spaceChildren ?: emptyList(),
spaceSummary = spaceSum?.let { Success(spaceSum) } ?: Loading()
) )
} }
@ -101,9 +102,14 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val query = session.spaceService().querySpaceChildren(initialState.spaceId) val query = session.spaceService().querySpaceChildren(initialState.spaceId)
val knownSummaries = query.second.mapNotNull {
session.getRoomSummary(it.childRoomId)
?.takeIf { it.membership == Membership.JOIN } // only take if joined because it will be up to date (synced)
}
setState { setState {
copy( copy(
spaceSummaryApiResult = Success(query.second) spaceSummaryApiResult = Success(query.second),
knownRoomSummaries = knownSummaries
) )
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
@ -148,7 +154,7 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
copy(hierarchyStack = hierarchyStack + listOf(action.spaceChildInfo.childRoomId)) copy(hierarchyStack = hierarchyStack + listOf(action.spaceChildInfo.childRoomId))
} }
} }
SpaceDirectoryViewAction.HandleBack -> { SpaceDirectoryViewAction.HandleBack -> {
withState { withState {
if (it.hierarchyStack.isEmpty()) { if (it.hierarchyStack.isEmpty()) {
_viewEvents.post(SpaceDirectoryViewEvents.Dismiss) _viewEvents.post(SpaceDirectoryViewEvents.Dismiss)
@ -161,20 +167,20 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
} }
} }
} }
is SpaceDirectoryViewAction.JoinOrOpen -> { is SpaceDirectoryViewAction.JoinOrOpen -> {
handleJoinOrOpen(action.spaceChildInfo) handleJoinOrOpen(action.spaceChildInfo)
} }
is SpaceDirectoryViewAction.NavigateToRoom -> { is SpaceDirectoryViewAction.NavigateToRoom -> {
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToRoom(action.roomId)) _viewEvents.post(SpaceDirectoryViewEvents.NavigateToRoom(action.roomId))
} }
is SpaceDirectoryViewAction.ShowDetails -> { is SpaceDirectoryViewAction.ShowDetails -> {
// This is temporary for now to at least display something for the space beta // This is temporary for now to at least display something for the space beta
// It's not ideal as it's doing some peeking that is not needed. // It's not ideal as it's doing some peeking that is not needed.
session.permalinkService().createRoomPermalink(action.spaceChildInfo.childRoomId)?.let { session.permalinkService().createRoomPermalink(action.spaceChildInfo.childRoomId)?.let {
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToMxToBottomSheet(it)) _viewEvents.post(SpaceDirectoryViewEvents.NavigateToMxToBottomSheet(it))
} }
} }
SpaceDirectoryViewAction.Retry -> { SpaceDirectoryViewAction.Retry -> {
refreshFromApi() refreshFromApi()
} }
} }

View File

@ -22,7 +22,6 @@ import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
@ -33,12 +32,12 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.ButtonStateView import im.vector.app.core.platform.ButtonStateView
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.databinding.BottomSheetInvitedToSpaceBinding import im.vector.app.databinding.BottomSheetInvitedToSpaceBinding
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.matrixto.SpaceCardRenderer
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
@ -60,6 +59,9 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetIn
@Inject @Inject
lateinit var avatarRenderer: AvatarRenderer lateinit var avatarRenderer: AvatarRenderer
@Inject
lateinit var spaceCardRenderer: SpaceCardRenderer
private val viewModel: SpaceInviteBottomSheetViewModel by fragmentViewModel(SpaceInviteBottomSheetViewModel::class) private val viewModel: SpaceInviteBottomSheetViewModel by fragmentViewModel(SpaceInviteBottomSheetViewModel::class)
@Inject lateinit var viewModelFactory: SpaceInviteBottomSheetViewModel.Factory @Inject lateinit var viewModelFactory: SpaceInviteBottomSheetViewModel.Factory
@ -133,12 +135,7 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetIn
views.inviterMxid.isVisible = false views.inviterMxid.isVisible = false
} }
views.spaceCard.matrixToCardContentVisibility.isVisible = true spaceCardRenderer.render(summary, state.peopleYouKnow.invoke().orEmpty(), null, views.spaceCard)
summary?.toMatrixItem()?.let { avatarRenderer.renderSpace(it, views.spaceCard.matrixToCardAvatar) }
views.spaceCard.matrixToCardNameText.text = summary?.displayName
views.spaceCard.matrixToBetaTag.isVisible = true
views.spaceCard.matrixToCardAliasText.setTextOrHide(summary?.canonicalAlias)
views.spaceCard.matrixToCardDescText.setTextOrHide(summary?.topic)
views.spaceCard.matrixToCardMainButton.button.text = getString(R.string.accept) views.spaceCard.matrixToCardMainButton.button.text = getString(R.string.accept)
views.spaceCard.matrixToCardSecondaryButton.button.text = getString(R.string.decline) views.spaceCard.matrixToCardSecondaryButton.button.text = getString(R.string.decline)
@ -178,40 +175,6 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetIn
views.spaceCard.matrixToCardSecondaryButton.button.isEnabled = true views.spaceCard.matrixToCardSecondaryButton.button.isEnabled = true
} }
} }
val memberCount = summary?.otherMemberIds?.size ?: 0
if (memberCount != 0) {
views.spaceCard.matrixToMemberPills.isVisible = true
views.spaceCard.spaceChildMemberCountText.text = resources.getQuantityString(R.plurals.room_title_members, memberCount, memberCount)
} else {
// hide the pill
views.spaceCard.matrixToMemberPills.isVisible = false
}
val peopleYouKnow = state.peopleYouKnow.invoke().orEmpty()
val images = listOf(
views.spaceCard.knownMember1,
views.spaceCard.knownMember2,
views.spaceCard.knownMember3,
views.spaceCard.knownMember4,
views.spaceCard.knownMember5
).onEach { it.isGone = true }
if (peopleYouKnow.isEmpty()) {
views.spaceCard.peopleYouMayKnowText.isVisible = false
} else {
peopleYouKnow.forEachIndexed { index, item ->
images[index].isVisible = true
avatarRenderer.render(item.toMatrixItem(), images[index])
}
views.spaceCard.peopleYouMayKnowText.setTextOrHide(
resources.getQuantityString(R.plurals.space_people_you_know,
peopleYouKnow.count(),
peopleYouKnow.count()
)
)
}
} }
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetInvitedToSpaceBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetInvitedToSpaceBinding {

View File

@ -27,7 +27,6 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.RoomCategoryItem_ import im.vector.app.features.home.room.list.RoomCategoryItem_
import org.matrix.android.sdk.api.session.room.ResultBoundaries import org.matrix.android.sdk.api.session.room.ResultBoundaries
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.RoomType
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
@ -155,7 +154,6 @@ class AddRoomListController @Inject constructor(
id(item.roomId) id(item.roomId)
matrixItem(item.toMatrixItem()) matrixItem(item.toMatrixItem())
avatarRenderer(host.avatarRenderer) avatarRenderer(host.avatarRenderer)
space(item.roomType == RoomType.SPACE)
selected(host.selectedItems[item.roomId] ?: false) selected(host.selectedItems[item.roomId] ?: false)
itemClickListener(DebouncedClickListener({ itemClickListener(DebouncedClickListener({
host.listener?.onItemSelected(item) host.listener?.onItemSelected(item)

View File

@ -34,18 +34,14 @@ abstract class RoomManageSelectionItem : VectorEpoxyModel<RoomManageSelectionIte
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var space: Boolean = false
@EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var suggested: Boolean = false @EpoxyAttribute var suggested: Boolean = false
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
if (space) { avatarRenderer.render(matrixItem, holder.avatarImageView)
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView)
} else {
avatarRenderer.render(matrixItem, holder.avatarImageView)
}
holder.titleText.text = matrixItem.getBestName() holder.titleText.text = matrixItem.getBestName()
if (selected) { if (selected) {

View File

@ -33,17 +33,13 @@ abstract class RoomSelectionItem : VectorEpoxyModel<RoomSelectionItem.Holder>()
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var space: Boolean = false
@EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
if (space) { avatarRenderer.render(matrixItem, holder.avatarImageView)
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView)
} else {
avatarRenderer.render(matrixItem, holder.avatarImageView)
}
holder.titleText.text = matrixItem.getBestName() holder.titleText.text = matrixItem.getBestName()
if (selected) { if (selected) {

View File

@ -27,7 +27,6 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
@ -83,7 +82,6 @@ class SpaceManageRoomsController @Inject constructor(
matrixItem(childInfo.toMatrixItem()) matrixItem(childInfo.toMatrixItem())
avatarRenderer(host.avatarRenderer) avatarRenderer(host.avatarRenderer)
suggested(childInfo.suggested ?: false) suggested(childInfo.suggested ?: false)
space(childInfo.roomType == RoomType.SPACE)
selected(data.selectedRooms.contains(childInfo.childRoomId)) selected(data.selectedRooms.contains(childInfo.childRoomId))
itemClickListener(DebouncedClickListener({ itemClickListener(DebouncedClickListener({
host.listener?.toggleSelection(childInfo) host.listener?.toggleSelection(childInfo)

View File

@ -71,7 +71,7 @@ class SpaceSettingsController @Inject constructor(
// Use the current value // Use the current value
avatarRenderer(host.avatarRenderer) avatarRenderer(host.avatarRenderer)
// We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. // We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar.
matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl)) matrixItem(roomSummary.toMatrixItem().updateAvatar(data.currentRoomAvatarUrl))
} }
RoomSettingsViewState.AvatarAction.DeleteAvatar -> RoomSettingsViewState.AvatarAction.DeleteAvatar ->
imageUri(null) imageUri(null)

View File

@ -139,7 +139,7 @@ class SpaceSettingsFragment @Inject constructor(
drawableProvider.getDrawable(R.drawable.ic_beta_pill), drawableProvider.getDrawable(R.drawable.ic_beta_pill),
null null
) )
avatarRenderer.renderSpace(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView) avatarRenderer.render(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView)
views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel)
} }

Some files were not shown because too many files have changed in this diff Show More