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> {
companion object {
const val BRAND_GOOGLE = "org.matrix.google"
const val BRAND_GITHUB = "org.matrix.github"
const val BRAND_APPLE = "org.matrix.apple"
const val BRAND_FACEBOOK = "org.matrix.facebook"
const val BRAND_TWITTER = "org.matrix.twitter"
const val BRAND_GITLAB = "org.matrix.gitlab"
const val BRAND_GOOGLE = "google"
const val BRAND_GITHUB = "github"
const val BRAND_APPLE = "apple"
const val BRAND_FACEBOOK = "facebook"
const val BRAND_TWITTER = "twitter"
const val BRAND_GITLAB = "gitlab"
}
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 isOutgoing: Boolean
val roomId: String
val opponentUserId: String
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
}
val ourPartyId: String
var opponentPartyId: Optional<String>?
var opponentVersion: Int
var capabilities: CallCapabilities?
var state: CallState
/**
@ -91,8 +89,12 @@ interface MxCall : MxCallDetail {
/**
* 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 removeListener(listener: StateListener)

View File

@ -54,7 +54,7 @@ interface PermalinkService {
*
* @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]

View File

@ -32,5 +32,7 @@ data class SpaceChildInfo(
val parentRoomId: String?,
val suggested: Boolean?,
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")
USER_HANGUP,
@Json(name = "replaced")
REPLACED,
@Json(name = "user_media_failed")
USER_MEDIA_FAILED,

View File

@ -38,23 +38,23 @@ data class CallReplacesContent(
*/
@Json(name = "replacement_id") val replacementId: String? = null,
/**
* 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.
* If absent, the transferee contacts the Matrix User ID given in the target_user field in a room of its choosing.
* 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.
* 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,
/**
* If specified, gives the call ID for the transferee's client to use when placing the replacement call.
* Mutually exclusive with await_call
* If specified, gives the call ID for the transferee's client to use when placing the replacement call.
* Mutually exclusive with await_call
*/
@Json(name = "create_call") val createCall: String? = null,
/**
* If specified, gives the call ID that the transferee's client should wait for.
* Mutually exclusive with create_call.
* If specified, gives the call ID that the transferee's client should wait for.
* Mutually exclusive with create_call.
*/
@Json(name = "await_call") val awaitCall: String? = null,
/**
@ -77,6 +77,5 @@ data class CallReplacesContent(
* Optional. The avatar URL of the transfer target.
*/
@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_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 roomType: String?,
val viaServers: List<String>,
val someMembers: List<MatrixItem.UserItem>?
val someMembers: List<MatrixItem.UserItem>?,
val isPublic: Boolean
) : PeekResult()
data class PeekingNotAllowed(

View File

@ -22,16 +22,16 @@ import org.matrix.android.sdk.api.util.JsonDict
@JsonClass(generateAdapter = true)
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,
/*
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,
/*
Required. Information used to identify this third party location.
/**
* Required. Information used to identify this third party location.
*/
@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.room.model.RoomMemberSummary
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.roomdirectory.PublicRoom
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
@ -38,6 +39,8 @@ sealed class MatrixItem(
init {
if (BuildConfig.DEBUG) checkId()
}
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
}
data class EventItem(override val id: String,
@ -47,6 +50,8 @@ sealed class MatrixItem(
init {
if (BuildConfig.DEBUG) checkId()
}
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
}
data class RoomItem(override val id: String,
@ -56,6 +61,19 @@ sealed class MatrixItem(
init {
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,
@ -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
override fun getBestName() = id
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
}
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
override fun getBestName() = id
override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
}
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)
*/
fun getIdPrefix() = when (this) {
is UserItem -> '@'
is EventItem -> '$'
is SpaceItem,
is RoomItem -> '!'
is RoomAliasItem -> '#'
is GroupItem -> '+'
@ -148,7 +173,11 @@ fun User.toMatrixItem() = MatrixItem.UserItem(userId, 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)
@ -159,4 +188,8 @@ fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName,
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
*/
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"

View File

@ -88,11 +88,9 @@ internal class DefaultAuthenticationService @Inject constructor(
return buildString {
append(homeServerUrlBase)
append(SSO_REDIRECT_PATH)
if (providerId != null) {
append(MSC2858_SSO_REDIRECT_PATH)
append("/$providerId")
} else {
append(SSO_REDIRECT_PATH)
}
// Set the redirect url
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
* See MSC #2858
*/
@Json(name = "org.matrix.msc2858.identity_providers")
@Json(name = "identity_providers")
val ssoIdentityProvider: List<SsoIdentityProvider>? = null
)

View File

@ -16,6 +16,7 @@
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.SpaceChildInfo
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,
suggested = it.suggested,
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()

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.internal.SessionManager
import org.matrix.android.sdk.internal.di.SessionId
import timber.log.Timber
import javax.inject.Inject
@SessionScope
@ -43,15 +44,16 @@ internal class SessionListeners @Inject constructor(
fun dispatch(block: (Session, Session.Listener) -> Unit) {
synchronized(listeners) {
val session = getSession()
val session = getSession() ?: return Unit.also {
Timber.w("You don't have any attached session")
}
listeners.forEach {
tryOrNull { block(session, it) }
}
}
}
private fun getSession(): Session {
private fun getSession(): 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.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.CallCapabilities
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.CallNegotiateContent
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.CallSignalingContent
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@SessionScope
@ -192,6 +189,9 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
// Ignore remote echo
return
}
if (event.roomId == null || event.senderId == null) {
return
}
if (event.senderId == userId) {
// discard current call, it's answered by another of my session
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}")
return
}
call.apply {
opponentPartyId = Optional.from(content.partyId)
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
capabilities = content.capabilities ?: CallCapabilities()
}
mxCallFactory.updateOutgoingCallWithOpponentData(call, event.senderId, content, content.capabilities)
callListenersDispatcher.onCallAnswerReceived(content)
}
}

View File

@ -17,18 +17,17 @@
package org.matrix.android.sdk.internal.session.call
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.room.model.call.CallCapabilities
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.UserId
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.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import java.math.BigDecimal
import java.util.UUID
import javax.inject.Inject
internal class MxCallFactory @Inject constructor(
@ -48,32 +47,38 @@ internal class MxCallFactory @Inject constructor(
roomId = roomId,
userId = userId,
ourPartyId = deviceId ?: "",
opponentUserId = opponentUserId,
isVideoCall = content.isVideo(),
localEchoEventFactory = localEchoEventFactory,
eventSenderProcessor = eventSenderProcessor,
matrixConfiguration = matrixConfiguration,
getProfileInfoTask = getProfileInfoTask
).apply {
opponentPartyId = Optional.from(content.partyId)
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
capabilities = content.capabilities ?: CallCapabilities()
updateOpponentData(opponentUserId, content, content.capabilities)
}
}
fun createOutgoingCall(roomId: String, opponentUserId: String, isVideoCall: Boolean): MxCall {
return MxCallImpl(
callId = UUID.randomUUID().toString(),
callId = CallIdGenerator.generate(),
isOutgoing = true,
roomId = roomId,
userId = userId,
ourPartyId = deviceId ?: "",
opponentUserId = opponentUserId,
isVideoCall = isVideoCall,
localEchoEventFactory = localEchoEventFactory,
eventSenderProcessor = eventSenderProcessor,
matrixConfiguration = matrixConfiguration,
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
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.MxCall
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.CallReplacesContent
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.util.Optional
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.queue.EventSenderProcessor
import timber.log.Timber
import java.util.UUID
import java.math.BigDecimal
internal class MxCallImpl(
override val callId: String,
override val isOutgoing: Boolean,
override val roomId: String,
private val userId: String,
override val opponentUserId: String,
override val isVideoCall: Boolean,
override val ourPartyId: String,
private val localEchoEventFactory: LocalEchoEventFactory,
@ -61,8 +62,16 @@ internal class MxCallImpl(
override var opponentPartyId: Optional<String>? = null
override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION
override lateinit var opponentUserId: String
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
set(value) {
field = value
@ -202,7 +211,10 @@ internal class MxCallImpl(
.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 profileInfo = try {
getProfileInfoTask.execute(profileInfoParams)
@ -213,15 +225,16 @@ internal class MxCallImpl(
CallReplacesContent(
callId = callId,
partyId = ourPartyId,
replacementId = UUID.randomUUID().toString(),
replacementId = CallIdGenerator.generate(),
version = MxCall.VOIP_PROTO_VERSION.toString(),
targetUser = CallReplacesContent.TargetUser(
id = targetUserId,
displayName = profileInfo?.get(ProfileService.DISPLAY_NAME_KEY) as? String,
avatarUrl = profileInfo?.get(ProfileService.AVATAR_URL_KEY) as? String
),
targerRoomId = targetRoomId,
createCall = UUID.randomUUID().toString()
targetRoomId = targetRoomId,
awaitCall = awaitCallId,
createCall = createCallId
)
.let { createEventAndLocalEcho(type = EventType.CALL_REPLACES, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }

View File

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

View File

@ -33,8 +33,8 @@ internal class DefaultPermalinkService @Inject constructor(
return permalinkFactory.createPermalink(id)
}
override fun createRoomPermalink(roomId: String): String? {
return permalinkFactory.createRoomPermalink(roomId)
override fun createRoomPermalink(roomId: String, viaServers: List<String>?): String? {
return permalinkFactory.createRoomPermalink(roomId, viaServers)
}
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)
}
fun createRoomPermalink(roomId: String): String? {
fun createRoomPermalink(roomId: String, via: List<String>? = null): String? {
return if (roomId.isEmpty()) {
null
} 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.
*/
fun computeViaParams(userId: String, roomId: String): String {
return computeViaParams(userId, roomId, 3)
.joinToString(prefix = "?via=", separator = "&via=") { URLEncoder.encode(it, "utf-8") }
return asUrlViaParameters(computeViaParams(userId, roomId, 3))
}
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> {

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.RoomCanonicalAliasContent
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.RoomNameContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
@ -105,7 +107,8 @@ internal class DefaultPeekRoomTask @Inject constructor(
numJoinedMembers = publicRepoResult.numJoinedMembers,
viaServers = serverList,
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
.lastOrNull { it.type == EventType.STATE_ROOM_CREATE }
?.content
@ -158,7 +166,8 @@ internal class DefaultPeekRoomTask @Inject constructor(
numJoinedMembers = memberCount,
roomType = roomType,
viaServers = serverList,
someMembers = someMembers
someMembers = someMembers,
isPublic = historyVisibility == RoomHistoryVisibility.WORLD_READABLE
)
} catch (failure: Throwable) {
// Would be M_FORBIDDEN if cannot peek :/

View File

@ -147,7 +147,8 @@ internal class DefaultSpaceService @Inject constructor(
parentRoomId = childStateEv.roomId,
suggested = childStateEvContent.suggested,
canonicalAlias = childSummary.canonicalAlias,
aliases = childSummary.aliases
aliases = childSummary.aliases,
worldReadable = childSummary.worldReadable
)
}
}.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() {
repeat(supportFragmentManager.backStackEntryCount) {
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) {
is CallState.Idle,
is CallState.CreateOffer,
is CallState.Dialing -> {
is CallState.Dialing -> {
views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true
views.callStatusText.setText(R.string.call_ring)
@ -189,16 +189,27 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
configureCallInfo(state)
}
is CallState.Answering -> {
is CallState.Answering -> {
views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true
views.callStatusText.setText(R.string.call_connecting)
views.callConnectingProgress.isVisible = true
configureCallInfo(state)
}
is CallState.Connected -> {
is CallState.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.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true
@ -220,7 +231,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
if (callArgs.isVideoCall) {
views.callVideoGroup.isVisible = true
views.callInfoGroup.isVisible = false
views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null
views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null
} else {
views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true
@ -235,10 +246,10 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.callConnectingProgress.isVisible = true
}
}
is CallState.Terminated -> {
is CallState.Terminated -> {
finish()
}
null -> {
null -> {
}
}
}
@ -247,7 +258,11 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
state.callInfo.otherUserItem?.let {
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen)
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) {
avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter)
} else {
@ -322,13 +337,13 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
private fun handleViewEvents(event: VectorCallViewEvents?) {
Timber.v("## VOIP handleViewEvents $event")
when (event) {
VectorCallViewEvents.DismissNoCall -> {
VectorCallViewEvents.DismissNoCall -> {
finish()
}
is VectorCallViewEvents.ConnectionTimeout -> {
is VectorCallViewEvents.ConnectionTimeout -> {
onErrorTimoutConnect(event.turn)
}
is VectorCallViewEvents.ShowDialPad -> {
is VectorCallViewEvents.ShowDialPad -> {
CallDialPadBottomSheet.newInstance(false).apply {
callback = dialPadCallback
}.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG)
@ -336,7 +351,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
is VectorCallViewEvents.ShowCallTransferScreen -> {
navigator.openCallTransfer(this, callArgs.callId)
}
null -> {
null -> {
}
}
}

View File

@ -34,4 +34,5 @@ sealed class VectorCallViewActions : VectorViewModelAction {
object ToggleCamera : VectorCallViewActions()
object ToggleHDSD : 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.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.call.audio.CallAudioManager
@ -111,12 +111,21 @@ class VectorCallViewModel @AssistedInject constructor(
setState {
copy(
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 {
override fun onCurrentCallChange(call: WebRtcCall?) {
@ -166,7 +175,7 @@ class VectorCallViewModel @AssistedInject constructor(
} else {
call = webRtcCall
callManager.addCurrentCallListener(currentCallListener)
val item = webRtcCall.getOpponentAsMatrixItem(session)
val item = webRtcCall.getOpponentAsMatrixItem(session)
webRtcCall.addListener(callListener)
val currentSoundDevice = callManager.audioManager.selectedDevice
if (currentSoundDevice == CallAudioManager.Device.PHONE) {
@ -185,7 +194,8 @@ class VectorCallViewModel @AssistedInject constructor(
canSwitchCamera = webRtcCall.canSwitchCamera(),
formattedDuration = webRtcCall.formattedDuration(),
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)
@ -201,27 +211,27 @@ class VectorCallViewModel @AssistedInject constructor(
override fun handle(action: VectorCallViewActions) = withState { state ->
when (action) {
VectorCallViewActions.EndCall -> call?.endCall()
VectorCallViewActions.AcceptCall -> {
VectorCallViewActions.EndCall -> call?.endCall()
VectorCallViewActions.AcceptCall -> {
setState {
copy(callState = Loading())
}
call?.acceptIncomingCall()
}
VectorCallViewActions.DeclineCall -> {
VectorCallViewActions.DeclineCall -> {
setState {
copy(callState = Loading())
}
call?.endCall()
}
VectorCallViewActions.ToggleMute -> {
VectorCallViewActions.ToggleMute -> {
val muted = state.isAudioMuted
call?.muteCall(!muted)
setState {
copy(isAudioMuted = !muted)
}
}
VectorCallViewActions.ToggleVideo -> {
VectorCallViewActions.ToggleVideo -> {
if (state.isVideoCall) {
val videoEnabled = state.isVideoEnabled
call?.enableVideo(!videoEnabled)
@ -231,14 +241,14 @@ class VectorCallViewModel @AssistedInject constructor(
}
Unit
}
VectorCallViewActions.ToggleHoldResume -> {
VectorCallViewActions.ToggleHoldResume -> {
val isRemoteOnHold = state.isRemoteOnHold
call?.updateRemoteOnHold(!isRemoteOnHold)
}
is VectorCallViewActions.ChangeAudioDevice -> {
callManager.audioManager.setAudioDevice(action.device)
}
VectorCallViewActions.SwitchSoundDevice -> {
VectorCallViewActions.SwitchSoundDevice -> {
_viewEvents.post(
VectorCallViewEvents.ShowSoundDeviceChooser(state.availableDevices, state.device)
)
@ -254,17 +264,17 @@ class VectorCallViewModel @AssistedInject constructor(
}
Unit
}
VectorCallViewActions.ToggleCamera -> {
VectorCallViewActions.ToggleCamera -> {
call?.switchCamera()
}
VectorCallViewActions.ToggleHDSD -> {
VectorCallViewActions.ToggleHDSD -> {
if (!state.isVideoCall) return@withState
call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD)
}
VectorCallViewActions.OpenDialPad -> {
VectorCallViewActions.OpenDialPad -> {
_viewEvents.post(VectorCallViewEvents.ShowDialPad)
}
is VectorCallViewActions.SendDtmfDigit -> {
is VectorCallViewActions.SendDtmfDigit -> {
call?.sendDtmfDigit(action.digit)
}
VectorCallViewActions.InitiateCallTransfer -> {
@ -272,9 +282,20 @@ class VectorCallViewModel @AssistedInject constructor(
VectorCallViewEvents.ShowCallTransferScreen
)
}
VectorCallViewActions.TransferCall -> {
handleCallTransfer()
}
}.exhaustive
}
private fun handleCallTransfer() {
viewModelScope.launch {
val currentCall = call ?: return@launch
val transfereeCall = callManager.getTransfereeForCallId(currentCall.callId) ?: return@launch
currentCall.transferToCall(transfereeCall)
}
}
@AssistedFactory
interface Factory {
fun create(initialState: VectorCallViewState): VectorCallViewModel

View File

@ -41,15 +41,22 @@ data class VectorCallViewState(
val otherKnownCallInfo: CallInfo? = null,
val callInfo: CallInfo = CallInfo(callId),
val formattedDuration: String = "",
val canOpponentBeTransferred: Boolean = false
val canOpponentBeTransferred: Boolean = false,
val transferee: TransfereeState = TransfereeState.NoTransferee
) : MvRxState {
sealed class TransfereeState {
object NoTransferee : TransfereeState()
data class KnownTransferee(val name: String) : TransfereeState()
object UnknownTransferee : TransfereeState()
}
data class CallInfo(
val callId: String,
val otherUserItem: MatrixItem? = null
)
constructor(callArgs: CallArgs): this(
constructor(callArgs: CallArgs) : this(
callId = callArgs.callId,
roomId = callArgs.signalingRoomId,
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.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper
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.MxCall
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
private val dialPadLookup: DialPadLookup,
callManager: WebRtcCallManager)
private val directRoomHelper: DirectRoomHelper,
private val callManager: WebRtcCallManager)
: VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
@AssistedFactory
@ -75,7 +78,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
override fun handle(action: CallTransferAction) {
when (action) {
is CallTransferAction.ConnectWithUserId -> connectWithUserId(action)
is CallTransferAction.ConnectWithUserId -> connectWithUserId(action)
is CallTransferAction.ConnectWithPhoneNumber -> connectWithPhoneNumber(action)
}.exhaustive
}
@ -83,8 +86,17 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) {
viewModelScope.launch {
try {
_viewEvents.post(CallTransferViewEvents.Loading)
call?.mxCall?.transfer(action.selectedUserId, null)
if (action.consultFirst) {
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)
} catch (failure: Throwable) {
_viewEvents.post(CallTransferViewEvents.FailToTransfer)
@ -97,7 +109,16 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
try {
_viewEvents.post(CallTransferViewEvents.Loading)
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)
} catch (failure: Throwable) {
_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.tryOrNull
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.MxCall
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 val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints()
class WebRtcCall(val mxCall: MxCall,
// This is where the call is placed from an ui perspective. In case of virtual room, it can differs from the signalingRoomId.
val nativeRoomId: String,
private val rootEglBase: EglBase?,
private val context: Context,
private val dispatcher: CoroutineContext,
private val sessionProvider: Provider<Session?>,
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
private val onCallBecomeActive: (WebRtcCall) -> Unit,
private val onCallEnded: (String) -> Unit) : MxCall.StateListener {
class WebRtcCall(
val mxCall: MxCall,
// This is where the call is placed from an ui perspective.
// In case of virtual room, it can differs from the signalingRoomId.
val nativeRoomId: String,
private val rootEglBase: EglBase?,
private val context: Context,
private val dispatcher: CoroutineContext,
private val sessionProvider: Provider<Session?>,
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
private val onCallBecomeActive: (WebRtcCall) -> Unit,
private val onCallEnded: (String) -> Unit
) : MxCall.StateListener {
interface Listener : MxCall.StateListener {
fun onCaptureStateChanged() {}
@ -118,6 +122,7 @@ class WebRtcCall(val mxCall: MxCall,
}
val callId = mxCall.callId
// room where call signaling is placed. In case of virtual room it can differs from the nativeRoomId.
val signalingRoomId = mxCall.roomId
@ -271,7 +276,7 @@ class WebRtcCall(val mxCall: MxCall,
sessionScope?.launch(dispatcher) {
when (mode) {
VectorCallActivity.INCOMING_ACCEPT -> {
VectorCallActivity.INCOMING_ACCEPT -> {
internalAcceptIncomingCall()
}
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() {
sessionScope?.launch {
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) {
return
}
@ -744,9 +783,9 @@ class WebRtcCall(val mxCall: MxCall,
mxCall.state = CallState.Terminated
sessionScope?.launch(dispatcher) {
release()
onCallEnded(callId)
}
onCallEnded(callId)
if (originatedByMe) {
if (sendEndSignaling) {
if (wasRinging) {
mxCall.reject()
} else {

View File

@ -147,6 +147,11 @@ class WebRtcCallManager @Inject constructor(
private val callsByCallId = ConcurrentHashMap<String, 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? {
return callsByCallId[callId]
}
@ -155,6 +160,10 @@ class WebRtcCallManager @Inject constructor(
return callsByRoomId[roomId] ?: emptyList()
}
fun getTransfereeForCallId(callId: String): WebRtcCall? {
return transferees[callId]
}
fun getCurrentCall(): WebRtcCall? {
return currentCall.get()
}
@ -229,34 +238,31 @@ class WebRtcCallManager @Inject constructor(
CallService.onCallTerminated(context, callId)
callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall)
callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall)
transferees.remove(callId)
if (getCurrentCall()?.callId == callId) {
val otherCall = getCalls().lastOrNull()
currentCall.setAndNotify(otherCall)
}
// This must be done in this thread
executor.execute {
// There is no active calls
if (getCurrentCall() == null) {
Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one")
peerConnectionFactory?.dispose()
peerConnectionFactory = null
audioManager.setMode(CallAudioManager.Mode.DEFAULT)
// did we start background sync? so we should stop it
if (isInBackground) {
if (UPHelper.hasEndpoint(context)) {
currentSession?.stopAnyBackgroundSync()
} else {
// for fdroid we should not stop, it should continue syncing
// maybe we should restore default timeout/delay though?
}
// There is no active calls
if (getCurrentCall() == null) {
Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one")
peerConnectionFactory?.dispose()
peerConnectionFactory = null
audioManager.setMode(CallAudioManager.Mode.DEFAULT)
// did we start background sync? so we should stop it
if (isInBackground) {
if (UPHelper.hasEndpoint(context)) {
currentSession?.stopAnyBackgroundSync()
} else {
// 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) {
val signalingRoomId = callUserMapper?.getOrCreateVirtualRoomForRoom(nativeRoomId, otherUserId) ?: nativeRoomId
suspend fun startOutgoingCall(nativeRoomId: String, otherUserId: String, isVideoCall: Boolean, transferee: WebRtcCall? = null) {
val signalingRoomId = callUserMapper?.getOrCreateVirtualRoomForRoom(nativeRoomId, otherUserId) ?: nativeRoomId
Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
if (getCallsByRoomId(nativeRoomId).isNotEmpty()) {
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 webRtcCall = createWebRtcCall(mxCall, nativeRoomId)
currentCall.setAndNotify(webRtcCall)
if (transferee != null) {
transferees[webRtcCall.callId] = transferee
}
CallService.onOutgoingCallRinging(
context = context.applicationContext,
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),
DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, 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),
ADD_TO_SPACE("/addToSpace", "spaceId", R.string.command_description_create_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()
ParsedCommand.SendChatEffect(ChatEffect.CONFETTI, message)
}
Command.SNOW.command -> {
val message = textMessage.substring(Command.SNOW.command.length).trim()
ParsedCommand.SendChatEffect(ChatEffect.SNOW, message)
Command.SNOWFALL.command -> {
val message = textMessage.substring(Command.SNOWFALL.command.length).trim()
ParsedCommand.SendChatEffect(ChatEffect.SNOWFALL, message)
}
Command.CREATE_SPACE.command -> {
val rawCommand = textMessage.substring(Command.CREATE_SPACE.command.length).trim()

View File

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

View File

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

View File

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

View File

@ -66,24 +66,24 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
DrawableImageViewTarget(imageView))
}
@UiThread
fun renderSpace(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) {
val placeholder = getSpacePlaceholderDrawable(matrixItem)
val resolvedUrl = resolvedUrl(matrixItem.avatarUrl)
glideRequests
.load(resolvedUrl)
.transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8))))
.placeholder(placeholder)
.into(DrawableImageViewTarget(imageView))
}
fun renderSpace(matrixItem: MatrixItem, imageView: ImageView) {
renderSpace(
matrixItem,
imageView,
GlideApp.with(imageView)
)
}
// fun renderSpace(matrixItem: MatrixItem, imageView: ImageView) {
// renderSpace(
// matrixItem,
// imageView,
// GlideApp.with(imageView)
// )
// }
//
// @UiThread
// private fun renderSpace(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) {
// val placeholder = getSpacePlaceholderDrawable(matrixItem)
// val resolvedUrl = resolvedUrl(matrixItem.avatarUrl)
// glideRequests
// .load(resolvedUrl)
// .transform(MultiTransformation(CenterCrop(), RoundedCorners(dimensionConverter.dpToPx(8))))
// .placeholder(placeholder)
// .into(DrawableImageViewTarget(imageView))
// }
fun clear(imageView: ImageView) {
// 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>) {
val placeholder = getPlaceholderDrawable(matrixItem)
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)
.into(target)
}
@ -197,17 +206,16 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
.beginConfig()
.bold()
.endConfig()
.buildRound(matrixItem.firstLetterOfDisplayName(), avatarColor)
}
@AnyThread
fun getSpacePlaceholderDrawable(matrixItem: MatrixItem): Drawable {
val avatarColor = matrixItemColorProvider.getColor(matrixItem)
return TextDrawable.builder()
.beginConfig()
.bold()
.endConfig()
.buildRoundRect(matrixItem.firstLetterOfDisplayName(), avatarColor, dimensionConverter.dpToPx(8))
.let {
when (matrixItem) {
is MatrixItem.SpaceItem -> {
it.buildRoundRect(matrixItem.firstLetterOfDisplayName(), avatarColor, dimensionConverter.dpToPx(8))
}
else -> {
it.buildRound(matrixItem.firstLetterOfDisplayName(), avatarColor)
}
}
}
}
// PRIVATE API *********************************************************************************

View File

@ -26,13 +26,13 @@ import javax.inject.Inject
enum class ChatEffect {
CONFETTI,
SNOW
SNOWFALL
}
fun ChatEffect.toMessageType(): String {
return when (this) {
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? {
return when (content.msgType) {
MessageType.MSGTYPE_CONFETTI -> ChatEffect.CONFETTI
MessageType.MSGTYPE_SNOW -> ChatEffect.SNOW
MessageType.MSGTYPE_SNOWFALL -> ChatEffect.SNOWFALL
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_TEXT -> {
event.root.getClearContent().toModel<MessageContent>()?.body
?.let { text ->
when {
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
}
}
@ -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)
.streamFor(150, 3000L)
}
ChatEffect.SNOW -> {
ChatEffect.SNOWFALL -> {
views.viewSnowFall.isVisible = true
views.viewSnowFall.restartFalling()
}

View File

@ -893,7 +893,7 @@ class RoomDetailViewModel @AssistedInject constructor(
if (sendChatEffect.message.isBlank()) {
val defaultMessage = stringProvider.getString(when (sendChatEffect.chatEffect) {
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)
} else {

View File

@ -30,4 +30,5 @@ sealed class RoomListAction : VectorViewModelAction {
data class ToggleTag(val roomId: String, val tag: String) : RoomListAction()
data class LeaveRoom(val roomId: 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)
roomListViewModel.observeViewEvents {
when (it) {
is RoomListViewEvents.Loading -> showLoading(it.message)
is RoomListViewEvents.Failure -> showFailure(it.throwable)
is RoomListViewEvents.SelectRoom -> handleSelectRoom(it)
is RoomListViewEvents.Done -> Unit
is RoomListViewEvents.Loading -> showLoading(it.message)
is RoomListViewEvents.Failure -> showFailure(it.throwable)
is RoomListViewEvents.SelectRoom -> handleSelectRoom(it)
is RoomListViewEvents.Done -> Unit
is RoomListViewEvents.NavigateToMxToBottomSheet -> handleShowMxToLink(it.link)
}.exhaustive
}
@ -155,6 +156,10 @@ class RoomListFragment @Inject constructor(
showErrorInSnackbar(throwable)
}
private fun handleShowMxToLink(link: String) {
navigator.openMatrixToBottomSheet(requireContext(), link)
}
override fun onDestroyView() {
adapterInfosList.onEach { it.contentEpoxyController.removeModelBuildListener(modelBuildListener) }
adapterInfosList.clear()
@ -474,6 +479,10 @@ class RoomListFragment @Inject constructor(
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) {
notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId)
roomListViewModel.handle(RoomListAction.RejectInvitation(room))

View File

@ -26,4 +26,5 @@ interface RoomListListener : FilteredRoomFooterItem.FilteredRoomFooterItemListen
fun onRejectRoomInvitation(room: RoomSummary)
fun onAcceptRoomInvitation(room: RoomSummary)
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()
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.ToggleSection -> handleToggleSection(action.section)
is RoomListAction.JoinSuggestedRoom -> handleJoinSuggestedRoom(action)
is RoomListAction.ShowRoomDetails -> handleShowRoomDetails(action)
}.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) {
session.getRoom(action.roomId)?.let { room ->
viewModelScope.launch(Dispatchers.IO) {

View File

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

View File

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

View File

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

View File

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

View File

@ -118,11 +118,9 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
session.getRoom(permalinkData.roomIdOrAlias)
}
?.roomSummary()
// don't take if not active, as it could be outdated
?.takeIf { it.membership.isActive() }
// XXX fix that
val forceRefresh = true
if (!forceRefresh && knownRoom != null) {
// don't take if not Join, as it could be outdated
?.takeIf { it.membership == Membership.JOIN }
if (knownRoom != null) {
setState {
copy(
roomPeekResult = Success(
@ -134,7 +132,8 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
alias = knownRoom.canonicalAlias,
membership = knownRoom.membership,
roomType = knownRoom.roomType,
viaServers = null
viaServers = null,
isPublic = knownRoom.isPublic
)
)
)
@ -150,7 +149,8 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
alias = peekResult.alias,
membership = knownRoom?.membership ?: Membership.NONE,
roomType = peekResult.roomType,
viaServers = peekResult.viaServers.takeIf { it.isNotEmpty() } ?: permalinkData.viaParameters
viaServers = peekResult.viaServers.takeIf { it.isNotEmpty() } ?: permalinkData.viaParameters,
isPublic = peekResult.isPublic
).also {
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
class MatrixToRoomSpaceFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer
private val avatarRenderer: AvatarRenderer,
private val spaceCardRenderer: SpaceCardRenderer
) : VectorBaseFragment<FragmentMatrixToRoomSpaceCardBinding>() {
private val sharedViewModel: MatrixToBottomSheetViewModel by parentFragmentViewModel()
@ -78,12 +79,19 @@ class MatrixToRoomSpaceFragment @Inject constructor(
when (val peek = item.invoke()) {
is RoomInfoResult.FullInfo -> {
val matrixItem = peek.roomItem
avatarRenderer.render(matrixItem, views.matrixToCardAvatar)
if (peek.roomType == RoomType.SPACE) {
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 {
views.matrixToBetaTag.isVisible = false
avatarRenderer.render(matrixItem, views.matrixToCardAvatar)
}
views.matrixToCardNameText.setTextOrHide(peek.name)
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 }
when (state.peopleYouKnow) {
is Success -> {
val someYouKnow = state.peopleYouKnow.invoke()
if (someYouKnow.isEmpty()) {
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()
)
)
}
spaceCardRenderer.renderPeopleYouKnow(views, someYouKnow)
}
else -> {
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 resolvedUrl = when (mode) {
Mode.FULL_SIZE,
Mode.STICKER -> resolveUrl(data)
Mode.STICKER -> resolveUrl(data)
Mode.THUMBNAIL -> contentUrlResolver.resolveThumbnail(data.url, size.width, size.height, ContentUrlResolver.ThumbnailMethod.SCALE)
}
// Fallback to base url
@ -313,7 +313,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
finalHeight = min(maxImageWidth * height / width, maxImageHeight)
finalWidth = finalHeight * width / height
}
Mode.STICKER -> {
Mode.STICKER -> {
// limit on width
val maxWidthDp = min(dimensionConverter.dpToPx(120), maxImageWidth / 2)
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.PinMode
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.roompreview.RoomPreviewActivity
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 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.thirdparty.RoomDirectoryData
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.WidgetType
@ -129,7 +129,7 @@ class DefaultNavigator @Inject constructor(
}
appStateHandler.setCurrentSpace(spaceId)
when (postSwitchSpaceAction) {
Navigator.PostSwitchSpaceAction.None -> {
Navigator.PostSwitchSpaceAction.None -> {
// 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
if (context is RoomDetailActivity) {
@ -139,7 +139,7 @@ class DefaultNavigator @Inject constructor(
Navigator.PostSwitchSpaceAction.OpenAddExistingRooms -> {
startActivity(context, SpaceManageActivity.newIntent(context, spaceId, ManageType.AddRooms), false)
}
is Navigator.PostSwitchSpaceAction.OpenDefaultRoom -> {
is Navigator.PostSwitchSpaceAction.OpenDefaultRoom -> {
val args = RoomDetailArgs(
postSwitchSpaceAction.roomId,
eventId = null,
@ -278,7 +278,7 @@ class DefaultNavigator @Inject constructor(
val intent = RoomDirectoryActivity.getIntent(context, initialFilter)
context.startActivity(intent)
}
is RoomGroupingMethod.BySpace -> {
is RoomGroupingMethod.BySpace -> {
val selectedSpace = groupingMethod.space()
if (selectedSpace == null) {
val intent = RoomDirectoryActivity.getIntent(context, initialFilter)
@ -320,7 +320,7 @@ class DefaultNavigator @Inject constructor(
val intent = InviteUsersToRoomActivity.getIntent(context, roomId)
context.startActivity(intent)
}
is RoomGroupingMethod.BySpace -> {
is RoomGroupingMethod.BySpace -> {
if (currentGroupingMethod.spaceSummary != null) {
// let user decides if he does it from space or room
(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.media.AttachmentData
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.settings.VectorSettingsActivity
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.thirdparty.RoomDirectoryData
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.util.MatrixItem
@ -44,7 +44,7 @@ interface Navigator {
sealed class PostSwitchSpaceAction {
object None : 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)

View File

@ -21,7 +21,6 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
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.thirdparty.RoomDirectoryData
data class PublicRoomsViewState(
// The current filter

View File

@ -17,7 +17,6 @@
package im.vector.app.features.roomdirectory
import im.vector.app.core.platform.VectorViewModelAction
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData
sealed class RoomDirectoryAction : VectorViewModelAction {
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.extensions.addFragment
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.databinding.ActivitySimpleBinding
import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment
@ -58,7 +59,7 @@ class RoomDirectoryActivity : VectorBaseActivity<ActivitySimpleBinding>() {
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
is RoomDirectorySharedAction.Back -> onBackPressed()
is RoomDirectorySharedAction.Back -> popBackstack()
is RoomDirectorySharedAction.CreateRoom -> {
// Transmit the filter to the CreateRoomFragment
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");
* you may not use this file except in compliance with the License.
@ -14,13 +14,12 @@
* 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(
/**
* The server name (might be null)
* 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)
*/
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
@ -40,15 +44,10 @@ data class RoomDirectoryData(
/**
* Tell if all the federated servers must be included
*/
val includeAllNetworks: Boolean = false,
/**
* the avatar url
*/
val avatarUrl: String? = null
val includeAllNetworks: Boolean = false
) {
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.roomdirectory.PublicRoomsFilter
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.rx.rx
import timber.log.Timber
@ -230,9 +229,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(
Timber.w("Try to join an already joining room. Should not happen")
return@withState
}
val viaServers = state.roomDirectoryData.homeServer
?.let { listOf(it) }
.orEmpty()
val viaServers = listOfNotNull(state.roomDirectoryData.homeServer)
viewModelScope.launch {
try {
session.joinRoom(action.roomId, viaServers = viaServers)

View File

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

View File

@ -16,10 +16,12 @@
package im.vector.app.features.roomdirectory.picker
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
@ -43,6 +45,9 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>()
@EpoxyAttribute
var includeAllNetworks: Boolean = false
@EpoxyAttribute
var checked: Boolean = false
@EpoxyAttribute
var globalListener: (() -> Unit)? = null
@ -63,6 +68,7 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>()
holder.nameView.text = directoryName
holder.descriptionView.setTextOrHide(directoryDescription)
holder.checkedView.isVisible = checked
}
class Holder : VectorEpoxyHolder() {
@ -71,5 +77,6 @@ abstract class RoomDirectoryItem : VectorEpoxyModel<RoomDirectoryItem.Holder>()
val avatarView by bind<ImageView>(R.id.itemRoomDirectoryAvatar)
val nameView by bind<TextView>(R.id.itemRoomDirectoryName)
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.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.room.model.thirdparty.RoomDirectoryData
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
import javax.inject.Inject
class RoomDirectoryListCreator @Inject constructor(private val stringArrayProvider: StringArrayProvider,
private val session: Session) {
class RoomDirectoryListCreator @Inject constructor(
private val stringArrayProvider: StringArrayProvider,
private val session: Session
) {
fun computeDirectories(thirdPartyProtocolData: Map<String, ThirdPartyProtocol>): List<RoomDirectoryData> {
val result = ArrayList<RoomDirectoryData>()
fun computeDirectories(thirdPartyProtocolData: Map<String, ThirdPartyProtocol>,
customHomeservers: Set<String>): List<RoomDirectoryServer> {
val result = ArrayList<RoomDirectoryServer>()
val protocols = ArrayList<RoomDirectoryData>()
// Add user homeserver name
val userHsName = session.myUserId.substringAfter(":")
result.add(RoomDirectoryData(
displayName = userHsName,
includeAllNetworks = true
))
// Add user's HS but for Matrix public rooms only
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 default protocol
protocols.add(
RoomDirectoryData(
homeServer = null,
displayName = RoomDirectoryData.MATRIX_PROTOCOL_NAME,
includeAllNetworks = false
)
)
// Add result of the request
thirdPartyProtocolData.forEach {
it.value.instances?.forEach { thirdPartyProtocolInstance ->
result.add(RoomDirectoryData(
homeServer = null,
displayName = thirdPartyProtocolInstance.desc ?: "",
thirdPartyInstanceId = thirdPartyProtocolInstance.instanceId,
includeAllNetworks = false,
// Default to protocol icon
avatarUrl = thirdPartyProtocolInstance.icon ?: it.value.icon
))
protocols.add(
RoomDirectoryData(
homeServer = null,
displayName = thirdPartyProtocolInstance.desc ?: "",
thirdPartyInstanceId = thirdPartyProtocolInstance.instanceId,
includeAllNetworks = false,
// 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
}
}

View File

@ -17,7 +17,14 @@
package im.vector.app.features.roomdirectory.picker
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.roomdirectory.RoomDirectoryServer
sealed class RoomDirectoryPickerAction : VectorViewModelAction {
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
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.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
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.loadingItem
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 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.net.ssl.HttpsURLConnection
class RoomDirectoryPickerController @Inject constructor(private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter,
private val roomDirectoryListCreator: RoomDirectoryListCreator
class RoomDirectoryPickerController @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter,
private val errorFormatter: ErrorFormatter
) : TypedEpoxyController<RoomDirectoryPickerViewState>() {
var currentRoomDirectoryData: RoomDirectoryData? = 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 asyncThirdPartyProtocol = viewState.asyncThirdPartyRequest
when (asyncThirdPartyProtocol) {
when (val asyncThirdPartyProtocol = data.asyncThirdPartyRequest) {
is Success -> {
val directories = roomDirectoryListCreator.computeDirectories(asyncThirdPartyProtocol())
directories.forEach {
buildDirectory(it)
data.directories.join(
each = { _, roomDirectoryServer -> buildDirectory(roomDirectoryServer) },
between = { idx, _ -> buildDivider(idx) }
)
buildForm(data)
verticalMarginItem {
id("space_bottom")
heightInPx(host.dimensionConverter.dpToPx(16))
}
}
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
roomDirectoryItem {
id(host.index++)
directoryName(roomDirectoryData.displayName)
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
if (data.inEditMode) {
verticalMarginItem {
id("form_space")
heightInPx(host.dimensionConverter.dpToPx(16))
}
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)
directoryAvatarUrl(roomDirectoryData.avatarUrl)
includeAllNetworks(roomDirectoryData.includeAllNetworks)
private fun getErrorMessage(error: Throwable): String {
return if (error is Failure.ServerError
&& error.httpCode == HttpsURLConnection.HTTP_INTERNAL_ERROR /* 500 */) {
stringProvider.getString(R.string.directory_add_a_new_server_error)
} else {
errorFormatter.toHumanReadable(error)
}
}
globalListener {
host.callback?.onRoomDirectoryClicked(roomDirectoryData)
private fun buildDivider(idx: Int) {
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 {
fun onRoomDirectoryClicked(roomDirectoryData: RoomDirectoryData)
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.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
@ -28,21 +27,22 @@ import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
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.databinding.FragmentRoomDirectoryPickerBinding
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.RoomDirectorySharedActionViewModel
import im.vector.app.features.roomdirectory.RoomDirectoryViewModel
import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryData
import timber.log.Timber
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,
private val roomDirectoryPickerController: RoomDirectoryPickerController
) : VectorBaseFragment<FragmentRoomDirectoryPickerBinding>(),
OnBackPressed,
RoomDirectoryPickerController.Callback {
private val viewModel: RoomDirectoryViewModel by activityViewModel()
@ -65,6 +65,11 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java)
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() {
@ -73,18 +78,6 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
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() {
views.roomDirectoryPickerList.configureWith(roomDirectoryPickerController)
roomDirectoryPickerController.callback = this
@ -97,6 +90,26 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
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() {
super.onResume()
(activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.select_room_directory)
@ -111,4 +124,16 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie
// Populate list with Epoxy
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.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
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.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.launch
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,
private val session: Session)
: VectorViewModel<RoomDirectoryPickerViewState, RoomDirectoryPickerAction, EmptyViewEvents>(initialState) {
class RoomDirectoryPickerViewModel @AssistedInject constructor(
@Assisted initialState: RoomDirectoryPickerViewState,
private val session: Session,
private val uiStateRepository: UiStateRepository,
private val stringProvider: StringProvider,
private val roomDirectoryListCreator: RoomDirectoryListCreator
) : VectorViewModel<RoomDirectoryPickerViewState, RoomDirectoryPickerAction, EmptyViewEvents>(initialState) {
@AssistedFactory
interface Factory {
@ -50,7 +60,22 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initial
}
init {
observeAndCompute()
load()
loadCustomRoomDirectoryHomeservers()
}
private fun observeAndCompute() {
selectSubscribe(
RoomDirectoryPickerViewState::asyncThirdPartyRequest,
RoomDirectoryPickerViewState::customHomeservers
) { async, custom ->
async()?.let {
setState {
copy(directories = roomDirectoryListCreator.computeDirectories(it, custom))
}
}
}
}
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) {
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.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.roomdirectory.RoomDirectoryServer
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
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

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.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding
import im.vector.app.features.roomdirectory.RoomDirectoryData
import kotlinx.parcelize.Parcelize
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 timber.log.Timber

View File

@ -68,16 +68,14 @@ class RoomSettingsController @Inject constructor(
id("avatar")
enabled(data.actionPermissions.canChangeAvatar)
when (val avatarAction = data.avatarAction) {
RoomSettingsViewState.AvatarAction.None -> {
RoomSettingsViewState.AvatarAction.None -> {
// Use the current value
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.
matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl))
matrixItem(roomSummary.toMatrixItem().updateAvatar(data.currentRoomAvatarUrl))
}
RoomSettingsViewState.AvatarAction.DeleteAvatar ->
imageUri(null)
is RoomSettingsViewState.AvatarAction.UpdateAvatar ->
imageUri(avatarAction.newAvatarUri)
RoomSettingsViewState.AvatarAction.DeleteAvatar -> imageUri(null)
is RoomSettingsViewState.AvatarAction.UpdateAvatar -> imageUri(avatarAction.newAvatarUri)
}
clickListener { host.callback?.onAvatarChange() }
deleteListener { host.callback?.onAvatarDelete() }
@ -102,6 +100,7 @@ class RoomSettingsController @Inject constructor(
id("topic")
enabled(data.actionPermissions.canChangeTopic)
value(data.newTopic ?: roomSummary.topic)
singleLine(false)
hint(host.stringProvider.getString(R.string.room_settings_topic_hint))
onTextChange { text ->

View File

@ -96,7 +96,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
val session = activeSessionHolder.getSafeActiveSession() ?: return
val roomSummary = session.getRoomSummary(spaceArgs.spaceId)
roomSummary?.toMatrixItem()?.let {
avatarRenderer.renderSpace(it, views.spaceAvatarImageView)
avatarRenderer.render(it, views.spaceAvatarImageView)
}
views.spaceNameView.text = roomSummary?.displayName
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.separator.isVisible = showSeparator
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView)
avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.counterBadgeView.render(countState)
}

View File

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

View File

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

View File

@ -54,7 +54,7 @@ class SpaceDetailEpoxyController @Inject constructor(
enabled(true)
imageUri(data?.avatarUri)
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() }
deleteListener { host.listener?.onAvatarDelete() }
}
@ -64,7 +64,6 @@ class SpaceDetailEpoxyController @Inject constructor(
enabled(true)
value(data?.name)
hint(host.stringProvider.getString(R.string.create_room_name_hint))
singleLine(true)
showBottomSeparator(false)
errorMessage(data?.nameInlineError)
// onBind { _, view, _ ->

View File

@ -122,13 +122,15 @@ class SpaceDirectoryController @Inject constructor(
val isSpace = info.roomType == RoomType.SPACE
val isJoined = data?.joinedRoomsIds?.contains(info.childRoomId) == true
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 {
id(info.childRoomId)
matrixItem(info.toMatrixItem())
matrixItem(matrixItem)
avatarRenderer(host.avatarRenderer)
topic(info.topic)
memberCount(info.activeMemberCount ?: 0)
space(isSpace)
loading(isLoading)
buttonLabel(
if (isJoined) host.stringProvider.getString(R.string.action_open)

View File

@ -16,6 +16,7 @@
package im.vector.app.features.spaces.explore
import android.content.DialogInterface
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
@ -23,19 +24,33 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
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.withState
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.configureWith
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.OnBackPressed
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.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.SpaceManageActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import java.net.URL
import javax.inject.Inject
@Parcelize
@ -44,9 +59,13 @@ data class SpaceDirectoryArgs(
) : Parcelable
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>(),
SpaceDirectoryController.InteractionListener,
TimelineEventController.UrlClickCallback,
OnBackPressed {
override fun getMenuRes() = R.menu.menu_space_directory
@ -71,6 +90,9 @@ class SpaceDirectoryFragment @Inject constructor(
viewModel.selectSubscribe(this, SpaceDirectoryState::canAddRooms) {
invalidateOptionsMenu()
}
views.spaceCard.matrixToCardMainButton.isVisible = false
views.spaceCard.matrixToCardSecondaryButton.isVisible = false
}
override fun onDestroyView() {
@ -82,10 +104,21 @@ class SpaceDirectoryFragment @Inject constructor(
override fun invalidate() = withState(viewModel) { 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 }
}?.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 ->
@ -96,7 +129,7 @@ class SpaceDirectoryFragment @Inject constructor(
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.spaceAddRoom -> {
R.id.spaceAddRoom -> {
withState(viewModel) { state ->
addExistingRooms(state.spaceId)
}
@ -138,6 +171,44 @@ class SpaceDirectoryFragment @Inject constructor(
override fun addExistingRooms(spaceId: String) {
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) {
// viewModel.handle(SpaceDirectoryViewAction.NavigateToRoom(roomId))
// }

View File

@ -37,7 +37,9 @@ data class SpaceDirectoryState(
val joinedRoomsIds: Set<String> = emptySet(),
// keys are room alias or roomId
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 {
constructor(args: SpaceDirectoryArgs) : this(
spaceId = args.spaceId

View File

@ -66,7 +66,8 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
val spaceSum = session.getRoomSummary(initialState.spaceId)
setState {
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) {
try {
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 {
copy(
spaceSummaryApiResult = Success(query.second)
spaceSummaryApiResult = Success(query.second),
knownRoomSummaries = knownSummaries
)
}
} catch (failure: Throwable) {
@ -148,7 +154,7 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
copy(hierarchyStack = hierarchyStack + listOf(action.spaceChildInfo.childRoomId))
}
}
SpaceDirectoryViewAction.HandleBack -> {
SpaceDirectoryViewAction.HandleBack -> {
withState {
if (it.hierarchyStack.isEmpty()) {
_viewEvents.post(SpaceDirectoryViewEvents.Dismiss)
@ -161,20 +167,20 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
}
}
}
is SpaceDirectoryViewAction.JoinOrOpen -> {
is SpaceDirectoryViewAction.JoinOrOpen -> {
handleJoinOrOpen(action.spaceChildInfo)
}
is SpaceDirectoryViewAction.NavigateToRoom -> {
is SpaceDirectoryViewAction.NavigateToRoom -> {
_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
// It's not ideal as it's doing some peeking that is not needed.
session.permalinkService().createRoomPermalink(action.spaceChildInfo.childRoomId)?.let {
_viewEvents.post(SpaceDirectoryViewEvents.NavigateToMxToBottomSheet(it))
}
}
SpaceDirectoryViewAction.Retry -> {
SpaceDirectoryViewAction.Retry -> {
refreshFromApi()
}
}

View File

@ -22,7 +22,6 @@ import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
@ -33,12 +32,12 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
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.VectorBaseBottomSheetDialogFragment
import im.vector.app.core.utils.toast
import im.vector.app.databinding.BottomSheetInvitedToSpaceBinding
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.matrixto.SpaceCardRenderer
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
@ -60,6 +59,9 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetIn
@Inject
lateinit var avatarRenderer: AvatarRenderer
@Inject
lateinit var spaceCardRenderer: SpaceCardRenderer
private val viewModel: SpaceInviteBottomSheetViewModel by fragmentViewModel(SpaceInviteBottomSheetViewModel::class)
@Inject lateinit var viewModelFactory: SpaceInviteBottomSheetViewModel.Factory
@ -133,12 +135,7 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetIn
views.inviterMxid.isVisible = false
}
views.spaceCard.matrixToCardContentVisibility.isVisible = true
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)
spaceCardRenderer.render(summary, state.peopleYouKnow.invoke().orEmpty(), null, views.spaceCard)
views.spaceCard.matrixToCardMainButton.button.text = getString(R.string.accept)
views.spaceCard.matrixToCardSecondaryButton.button.text = getString(R.string.decline)
@ -178,40 +175,6 @@ class SpaceInviteBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetIn
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 {

View File

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

View File

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

View File

@ -33,17 +33,13 @@ abstract class RoomSelectionItem : VectorEpoxyModel<RoomSelectionItem.Holder>()
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var space: Boolean = false
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
if (space) {
avatarRenderer.renderSpace(matrixItem, holder.avatarImageView)
} else {
avatarRenderer.render(matrixItem, holder.avatarImageView)
}
avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.titleText.text = matrixItem.getBestName()
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.utils.DebouncedClickListener
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.util.toMatrixItem
import javax.inject.Inject
@ -83,7 +82,6 @@ class SpaceManageRoomsController @Inject constructor(
matrixItem(childInfo.toMatrixItem())
avatarRenderer(host.avatarRenderer)
suggested(childInfo.suggested ?: false)
space(childInfo.roomType == RoomType.SPACE)
selected(data.selectedRooms.contains(childInfo.childRoomId))
itemClickListener(DebouncedClickListener({
host.listener?.toggleSelection(childInfo)

View File

@ -71,7 +71,7 @@ class SpaceSettingsController @Inject constructor(
// Use the current value
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.
matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl))
matrixItem(roomSummary.toMatrixItem().updateAvatar(data.currentRoomAvatarUrl))
}
RoomSettingsViewState.AvatarAction.DeleteAvatar ->
imageUri(null)

View File

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

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