diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt index 64b3e180aa..a0733dda97 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt @@ -51,12 +51,12 @@ data class SsoIdentityProvider( ) : Parcelable, Comparable { 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 { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallIdGenerator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallIdGenerator.kt new file mode 100644 index 0000000000..43e6872525 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallIdGenerator.kt @@ -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() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt index 7533619eb0..fcc9f7072d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -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? + 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? - 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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt index ac1d726d03..a6d4583c76 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt @@ -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? = null): String? /** * Creates a permalink for an event. If you have an event you can use [createPermalink] diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt index 66293bcb8c..8cd2a0538d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt @@ -32,5 +32,7 @@ data class SpaceChildInfo( val parentRoomId: String?, val suggested: Boolean?, val canonicalAlias: String?, - val aliases: List? + val aliases: List?, + val worldReadable: Boolean + ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt index 4752d777e1..9d6e1a7eae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt @@ -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, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt index 2b368a83a8..4559c5db6d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt @@ -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? - ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt index c96a800ee5..1e8959afc3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -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" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt index 888950dc12..b78cd5e032 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt @@ -28,7 +28,8 @@ sealed class PeekResult { val numJoinedMembers: Int?, val roomType: String?, val viaServers: List, - val someMembers: List? + val someMembers: List?, + val isPublic: Boolean ) : PeekResult() data class PeekingNotAllowed( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt index d77dfcfe35..246813a524 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt @@ -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 ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt index deb279eb95..3d2773fb4b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt @@ -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) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt index e0c52cf9ca..3742a429d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt @@ -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" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 46256f4b81..20ce438d8e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index d0d17e2cd5..c718fae390 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -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? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index 3fea15bd3d..c32c019625 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -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() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt index 5f529b3e66..d5c661b1e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt @@ -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.") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index 6bf11ab78f..61ea660b60 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -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) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt index b14cdca63c..547be2253f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt @@ -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) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt index 88fba0ea85..f101685a4b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -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? = 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) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt index fde3dd906c..82cd682eae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ThumbnailExtractor.kt @@ -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() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt index 7db9d8f68a..134da4ce51 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt @@ -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? { + return permalinkFactory.createRoomPermalink(roomId, viaServers) } override fun createPermalink(roomId: String, eventId: String): String { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt index 970752449a..639e45582a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt @@ -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? = 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) + ) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt index 0da60e9ba2..72fbfcced5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt @@ -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 { + return viaList.joinToString(prefix = "?via=", separator = "&via=") { URLEncoder.encode(it, "utf-8") } } fun computeViaParams(userId: String, roomId: String, max: Int): List { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt index c6f4bbb4e1..219e9c903f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt @@ -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()?.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 :/ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index d0ad19245f..9c6153b349 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -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() diff --git a/newsfragment/1458.feature b/newsfragment/1458.feature new file mode 100644 index 0000000000..ded4f549ed --- /dev/null +++ b/newsfragment/1458.feature @@ -0,0 +1 @@ +Allow user to add custom "network" in room search \ No newline at end of file diff --git a/newsfragment/3196.feature b/newsfragment/3196.feature new file mode 100644 index 0000000000..ca84dd51c8 --- /dev/null +++ b/newsfragment/3196.feature @@ -0,0 +1 @@ +Add Gitter.im as a default in the Change Network menu \ No newline at end of file diff --git a/newsfragment/3396.feature b/newsfragment/3396.feature new file mode 100644 index 0000000000..29ae6422bf --- /dev/null +++ b/newsfragment/3396.feature @@ -0,0 +1 @@ +Compress thumbnail: change Jpeg quality from 100 to 80 \ No newline at end of file diff --git a/newsfragment/3401.bugfix b/newsfragment/3401.bugfix new file mode 100644 index 0000000000..7f7539316d --- /dev/null +++ b/newsfragment/3401.bugfix @@ -0,0 +1 @@ +Fix | On Android it seems to be impossible to view the complete description of a Space (without dev tools) \ No newline at end of file diff --git a/newsfragment/3406.bugfix b/newsfragment/3406.bugfix new file mode 100644 index 0000000000..2623ccc1cd --- /dev/null +++ b/newsfragment/3406.bugfix @@ -0,0 +1 @@ +Fix | Suggest Rooms, Show a detailed view of the room on click \ No newline at end of file diff --git a/newsfragment/3420.feature b/newsfragment/3420.feature new file mode 100644 index 0000000000..3f3df52f62 --- /dev/null +++ b/newsfragment/3420.feature @@ -0,0 +1 @@ +VoIP: support attended transfer \ No newline at end of file diff --git a/newsfragment/3424.bugfix b/newsfragment/3424.bugfix new file mode 100644 index 0000000000..db66595cbd --- /dev/null +++ b/newsfragment/3424.bugfix @@ -0,0 +1 @@ +Fix app crashing when signing out \ No newline at end of file diff --git a/newsfragment/3430.feature b/newsfragment/3430.feature new file mode 100644 index 0000000000..ee9c269bcd --- /dev/null +++ b/newsfragment/3430.feature @@ -0,0 +1 @@ +/snow -> /snowfall and update wording (iso Element Web) \ No newline at end of file diff --git a/newsfragment/3442.bugfix b/newsfragment/3442.bugfix new file mode 100644 index 0000000000..5a2d08ec18 --- /dev/null +++ b/newsfragment/3442.bugfix @@ -0,0 +1 @@ +Switch to stable endpoint/fields for MSC2858 \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/core/extensions/Activity.kt b/vector/src/main/java/im/vector/app/core/extensions/Activity.kt index 55ec8b605e..de469b9e3a 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Activity.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Activity.kt @@ -94,6 +94,10 @@ fun AppCompatActivity.addFragmentToBackstack( } } +fun AppCompatActivity.popBackstack() { + supportFragmentManager.popBackStack() +} + fun AppCompatActivity.resetBackstack() { repeat(supportFragmentManager.backStackEntryCount) { supportFragmentManager.popBackStack() diff --git a/vector/src/main/java/im/vector/app/core/ui/list/VerticalMarginItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/VerticalMarginItem.kt new file mode 100644 index 0000000000..ec99c7c215 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/list/VerticalMarginItem.kt @@ -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() { + + @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(R.id.item_vertical_margin_space) + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index a4974283dc..ad04e33414 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -175,7 +175,7 @@ class VectorCallActivity : VectorBaseActivity(), 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(), 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(), 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(), CallContro views.callConnectingProgress.isVisible = true } } - is CallState.Terminated -> { + is CallState.Terminated -> { finish() } - null -> { + null -> { } } } @@ -247,7 +258,11 @@ class VectorCallActivity : VectorBaseActivity(), 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(), 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(), CallContro is VectorCallViewEvents.ShowCallTransferScreen -> { navigator.openCallTransfer(this, callArgs.callId) } - null -> { + null -> { } } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt index 7addabf724..a332153aaa 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt @@ -34,4 +34,5 @@ sealed class VectorCallViewActions : VectorViewModelAction { object ToggleCamera : VectorCallViewActions() object ToggleHDSD : VectorCallViewActions() object InitiateCallTransfer : VectorCallViewActions() + object TransferCall: VectorCallViewActions() } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 17163019ac..18eda0fd6f 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index 17f536e6cc..c5ae61cf60 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt index 5f661faf80..0f37ccaa29 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt @@ -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(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) diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 82d9d2e983..f2a008feb7 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -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, - private val peerConnectionFactoryProvider: Provider, - 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, + private val peerConnectionFactoryProvider: Provider, + 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 { diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 687c86d8f5..bb05d0ac11 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -147,6 +147,11 @@ class WebRtcCallManager @Inject constructor( private val callsByCallId = ConcurrentHashMap() private val callsByRoomId = ConcurrentHashMap>() + // 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() + 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) diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 0b210cf298..61d39857cc 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -46,7 +46,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d PLAIN("/plain", "", R.string.command_description_plain, false), DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session, false), CONFETTI("/confetti", "", R.string.command_confetti, false), - SNOW("/snow", "", R.string.command_snow, false), + SNOWFALL("/snowfall", "", R.string.command_snow, false), CREATE_SPACE("/createspace", " *", 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), diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index 9b190d64fe..3de00f4d0c 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt index b59b24fe55..47059128a1 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/SettingsContinueCancelItem.kt @@ -33,6 +33,9 @@ abstract class SettingsContinueCancelItem : EpoxyModelWithHolder() { 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() { @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() { 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 } diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt index 0a07d27f64..b02e5c52df 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt @@ -71,7 +71,7 @@ abstract class FormEditableSquareAvatarItem : EpoxyModelWithHolder { - avatarRenderer?.renderSpace(matrixItem!!, holder.image) + avatarRenderer?.render(matrixItem!!, holder.image) } else -> { avatarRenderer?.clear(holder.image) diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 65bc5e1200..c6cceee3b9 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -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) { 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 ********************************************************************************* diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt index e7136762d5..24151c5c10 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ChatEffectManager.kt @@ -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()?.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( "⛄️", "☃️", "❄️" diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index f13f9dba60..7fa36a39d7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 205ccf7fca..a2041c0a80 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt index 37f7d148aa..e6b6b34503 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt @@ -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?) : RoomListAction() + data class ShowRoomDetails(val roomId: String, val viaServers: List?) : RoomListAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index 76d7752ea7..8049d3c057 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -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)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt index 0ba265f841..cf619ce435 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListListener.kt @@ -26,4 +26,5 @@ interface RoomListListener : FilteredRoomFooterItem.FilteredRoomFooterItemListen fun onRejectRoomInvitation(room: RoomSummary) fun onAcceptRoomInvitation(room: RoomSummary) fun onJoinSuggestedRoom(room: SpaceChildInfo) + fun onSuggestedRoomClicked(room: SpaceChildInfo) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewEvents.kt index cb84d91373..df2ff58da6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 246d5052cf..fbb8faebb0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -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) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index e4826af04c..ec78a13c9f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -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>, - 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, diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt index cb9c8b1f2e..65b42f437b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SpaceChildInfoItem.kt @@ -48,7 +48,6 @@ abstract class SpaceChildInfoItem : VectorEpoxyModel( @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( 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 { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomListController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomListController.kt index 073fb43b6c..f9775967ee 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/SuggestedRoomListController.kt @@ -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)) } } } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt index 2f341d48ec..40213dc0ee 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetState.kt @@ -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? + val viaServers: List?, + val isPublic: Boolean ) : RoomInfoResult() data class PartialInfo( diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt index 694f324025..1c78348b56 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt index 04f72000fa..ad71d0b1b5 100644 --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToRoomSpaceFragment.kt @@ -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() { 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 diff --git a/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt b/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt new file mode 100644 index 0000000000..e51490a59c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/matrixto/SpaceCardRenderer.kt @@ -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, + 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, + 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) { + 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() + ) + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 1c0d859fa9..8d6d1f467b 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 3abf01583c..ed57ccb04c 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -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 -> diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 444c48bddb..cf0263a1e8 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsViewState.kt index 16e5428b9c..fdab72caba 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/PublicRoomsViewState.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt index a94cb7709f..77eec57ab3 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryAction.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt index d8edbcf503..9a63e81a2f 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryActivity.kt @@ -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() { .observe() .subscribe { sharedAction -> when (sharedAction) { - is RoomDirectorySharedAction.Back -> onBackPressed() + is RoomDirectorySharedAction.Back -> popBackstack() is RoomDirectorySharedAction.CreateRoom -> { // Transmit the filter to the CreateRoomFragment withState(roomDirectoryViewModel) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryData.kt similarity index 75% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt rename to vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryData.kt index 91f429d773..49bb769460 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/thirdparty/RoomDirectoryData.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryData.kt @@ -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" } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryServer.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryServer.kt new file mode 100644 index 0000000000..0f29ae5986 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryServer.kt @@ -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 +) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt index f64105b759..dc1cbfc58d 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/RoomDirectoryViewModel.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt index efb54650b8..aaf82c5791 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt @@ -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 -> diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt index 7b2e329b6a..7cf8e538ac 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryItem.kt @@ -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() @EpoxyAttribute var includeAllNetworks: Boolean = false + @EpoxyAttribute + var checked: Boolean = false + @EpoxyAttribute var globalListener: (() -> Unit)? = null @@ -63,6 +68,7 @@ abstract class RoomDirectoryItem : VectorEpoxyModel() holder.nameView.text = directoryName holder.descriptionView.setTextOrHide(directoryDescription) + holder.checkedView.isVisible = checked } class Holder : VectorEpoxyHolder() { @@ -71,5 +77,6 @@ abstract class RoomDirectoryItem : VectorEpoxyModel() val avatarView by bind(R.id.itemRoomDirectoryAvatar) val nameView by bind(R.id.itemRoomDirectoryName) val descriptionView by bind(R.id.itemRoomDirectoryDescription) + val checkedView by bind(R.id.itemRoomDirectoryChecked) } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryListCreator.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryListCreator.kt index d51ad5040b..65d8f2d1cb 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryListCreator.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryListCreator.kt @@ -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): List { - val result = ArrayList() + fun computeDirectories(thirdPartyProtocolData: Map, + customHomeservers: Set): List { + val result = ArrayList() + + val protocols = ArrayList() // 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 } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerAction.kt index 36f2cd4296..8be3c6b2b2 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt index 75e9807bd0..9a397c586d 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerController.kt @@ -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() { + 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) } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt index 7f205078f1..a32a3a897f 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt @@ -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(), + 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 + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt index d85b7937a2..2558715834 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt @@ -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(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(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 + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt index 61cf50e8dd..5cdee862ab 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewState.kt @@ -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> = Uninitialized + val asyncThirdPartyRequest: Async> = Uninitialized, + val customHomeservers: Set = emptySet(), + val inEditMode: Boolean = false, + val enteredServer: String = "", + val addServerAsync: Async = Uninitialized, + // computed + val directories: List = emptyList() ) : MvRxState diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt new file mode 100644 index 0000000000..6efb41d5b1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryServerItem.kt @@ -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() { + + @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(R.id.itemRoomDirectoryServerName) + val descriptionView by bind(R.id.itemRoomDirectoryServerDescription) + val deleteView by bind(R.id.itemRoomDirectoryServerRemove) + } +} diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt index 445d02d6e2..f9cf8e6dd7 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/roompreview/RoomPreviewActivity.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt index 24836bc504..6ee26ed54f 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt @@ -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 -> diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt index 66572f5a82..0efb0535e8 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt @@ -96,7 +96,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment() { holder.indentSpace.isVisible = indent > 0 holder.separator.isVisible = showSeparator - avatarRenderer.renderSpace(matrixItem, holder.avatarImageView) + avatarRenderer.render(matrixItem, holder.avatarImageView) holder.counterBadgeView.render(countState) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt b/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt index db58353e5c..977ab57bc9 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SubSpaceSummaryItem.kt @@ -81,7 +81,7 @@ abstract class SubSpaceSummaryItem : VectorEpoxyModel diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt index e334868d7c..e4ed431e73 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryController.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt index a866ea9b89..910c21926c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryFragment.kt @@ -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(), 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)) // } diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryState.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryState.kt index 220c3e3492..21541a51ab 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryState.kt @@ -37,7 +37,9 @@ data class SpaceDirectoryState( val joinedRoomsIds: Set = emptySet(), // keys are room alias or roomId val changeMembershipStates: Map = emptyMap(), - val canAddRooms: Boolean = false + val canAddRooms: Boolean = false, + // cached room summaries of known rooms + val knownRoomSummaries : List = emptyList() ) : MvRxState { constructor(args: SpaceDirectoryArgs) : this( spaceId = args.spaceId diff --git a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt index 313ddfe1dc..3d3e1dac65 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/explore/SpaceDirectoryViewModel.kt @@ -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() } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt index 8e536459a3..434fa613ab 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/invite/SpaceInviteBottomSheet.kt @@ -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 - 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 { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt index 69be246506..e0504d6531 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/AddRoomListController.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt index 9d188ac457..50e92c8758 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt @@ -34,18 +34,14 @@ abstract class RoomManageSelectionItem : VectorEpoxyModel() @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) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt index b16c6de921..f740938ee4 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt index 614f6f92c8..e0e7575f35 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt index 85c73ac8ef..7b405eb4f3 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt index b6f1fb6a4e..eb02ed7c2d 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SpacePreviewFragment.kt @@ -148,8 +148,8 @@ class SpacePreviewFragment @Inject constructor( // val roomPeekResult = preview.summary.roomPeekResult val spaceName = spacePreviewState.spaceInfo.invoke()?.name ?: spacePreviewState.name ?: "" val spaceAvatarUrl = spacePreviewState.spaceInfo.invoke()?.avatarUrl ?: spacePreviewState.avatarUrl - val mxItem = MatrixItem.RoomItem(spacePreviewState.idOrAlias, spaceName, spaceAvatarUrl) - avatarRenderer.renderSpace(mxItem, views.spacePreviewToolbarAvatar) + val mxItem = MatrixItem.SpaceItem(spacePreviewState.idOrAlias, spaceName, spaceAvatarUrl) + avatarRenderer.render(mxItem, views.spacePreviewToolbarAvatar) views.roomPreviewNoPreviewToolbarTitle.text = spaceName // } // is SpacePeekResult.SpacePeekError, diff --git a/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt b/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt index 367a81fe5a..1856edb61f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/preview/SubSpaceItem.kt @@ -48,8 +48,8 @@ abstract class SubSpaceItem : VectorEpoxyModel() { super.bind(holder) holder.nameText.text = title - avatarRenderer.renderSpace( - MatrixItem.RoomItem(roomId, title, avatarUrl), + avatarRenderer.render( + MatrixItem.SpaceItem(roomId, title, avatarUrl), holder.avatarImageView ) holder.tabView.tabDepth = depth diff --git a/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt b/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt index 1ec3a8ab46..e46c3516ca 100644 --- a/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt +++ b/vector/src/main/java/im/vector/app/features/ui/SharedPreferencesUiStateRepository.kt @@ -39,7 +39,7 @@ class SharedPreferencesUiStateRepository @Inject constructor( override fun getDisplayMode(): RoomListDisplayMode { return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) { VALUE_DISPLAY_MODE_PEOPLE -> RoomListDisplayMode.PEOPLE - VALUE_DISPLAY_MODE_ROOMS -> RoomListDisplayMode.ROOMS + VALUE_DISPLAY_MODE_ROOMS -> RoomListDisplayMode.ROOMS else -> if (vectorPreferences.labAddNotificationTab()) { RoomListDisplayMode.NOTIFICATIONS } else { @@ -89,6 +89,18 @@ class SharedPreferencesUiStateRepository @Inject constructor( return sharedPreferences.getBoolean("$KEY_SELECTED_METHOD@$sessionId", true) } + override fun setCustomRoomDirectoryHomeservers(sessionId: String, servers: Set) { + sharedPreferences.edit { + putStringSet("$KEY_CUSTOM_DIRECTORY_HOMESERVER@$sessionId", servers) + } + } + + override fun getCustomRoomDirectoryHomeservers(sessionId: String): Set { + return sharedPreferences.getStringSet("$KEY_CUSTOM_DIRECTORY_HOMESERVER@$sessionId", null) + .orEmpty() + .toSet() + } + companion object { private const val KEY_DISPLAY_MODE = "UI_STATE_DISPLAY_MODE" private const val VALUE_DISPLAY_MODE_CATCHUP = 0 @@ -98,5 +110,7 @@ class SharedPreferencesUiStateRepository @Inject constructor( private const val KEY_SELECTED_SPACE = "UI_STATE_SELECTED_SPACE" private const val KEY_SELECTED_GROUP = "UI_STATE_SELECTED_GROUP" private const val KEY_SELECTED_METHOD = "UI_STATE_SELECTED_METHOD" + + private const val KEY_CUSTOM_DIRECTORY_HOMESERVER = "KEY_CUSTOM_DIRECTORY_HOMESERVER" } } diff --git a/vector/src/main/java/im/vector/app/features/ui/UiStateRepository.kt b/vector/src/main/java/im/vector/app/features/ui/UiStateRepository.kt index 935da83f5d..3c48f8972d 100644 --- a/vector/src/main/java/im/vector/app/features/ui/UiStateRepository.kt +++ b/vector/src/main/java/im/vector/app/features/ui/UiStateRepository.kt @@ -32,6 +32,7 @@ interface UiStateRepository { fun storeDisplayMode(displayMode: RoomListDisplayMode) + // TODO Handle SharedPreference per session in a better way, also to cleanup when login out fun storeSelectedSpace(spaceId: String?, sessionId: String) fun storeSelectedGroup(groupId: String?, sessionId: String) @@ -40,4 +41,7 @@ interface UiStateRepository { fun getSelectedSpace(sessionId: String): String? fun getSelectedGroup(sessionId: String): String? fun isGroupingMethodSpace(sessionId: String): Boolean + + fun setCustomRoomDirectoryHomeservers(sessionId: String, servers: Set) + fun getCustomRoomDirectoryHomeservers(sessionId: String): Set } diff --git a/vector/src/main/res/layout/activity_call_transfer.xml b/vector/src/main/res/layout/activity_call_transfer.xml index 64ddd29319..5540eb91d3 100644 --- a/vector/src/main/res/layout/activity_call_transfer.xml +++ b/vector/src/main/res/layout/activity_call_transfer.xml @@ -52,7 +52,6 @@ android:layout_width="wrap_content" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:enabled="false" android:layout_height="wrap_content"/> + + + + + + app:layout_constraintTop_toBottomOf="@id/matrixToAccessText"> - + android:layout_height="wrap_content" + android:elevation="4dp"> - - + android:layout_height="match_parent" + android:theme="@style/Vector.Toolbar.Profile" + app:contentScrim="?riotx_background" + app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" + app:scrimAnimationDuration="250" + app:scrimVisibleHeightTrigger="120dp" + app:titleEnabled="false" + app:toolbarId="@+id/toolbar"> - + + + + + + + + + + + + + + - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_room_directory.xml b/vector/src/main/res/layout/item_room_directory.xml index 391f52ad92..19a457ec37 100644 --- a/vector/src/main/res/layout/item_room_directory.xml +++ b/vector/src/main/res/layout/item_room_directory.xml @@ -1,5 +1,4 @@ - + tools:src="@drawable/network_matrix" /> + app:layout_goneMarginEnd="@dimen/layout_horizontal_margin" + tools:text="@string/directory_server_native_rooms" + tools:visibility="visible" /> - + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_room_directory_server.xml b/vector/src/main/res/layout/item_room_directory_server.xml new file mode 100644 index 0000000000..5705e1c623 --- /dev/null +++ b/vector/src/main/res/layout/item_room_directory_server.xml @@ -0,0 +1,67 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_vertical_margin.xml b/vector/src/main/res/layout/item_vertical_margin.xml new file mode 100644 index 0000000000..fac46e47ea --- /dev/null +++ b/vector/src/main/res/layout/item_vertical_margin.xml @@ -0,0 +1,5 @@ + + diff --git a/vector/src/main/res/menu/menu_directory_server_picker.xml b/vector/src/main/res/menu/menu_directory_server_picker.xml deleted file mode 100644 index c544c80f8c..0000000000 --- a/vector/src/main/res/menu/menu_directory_server_picker.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/values/config.xml b/vector/src/main/res/values/config.xml index eb0cfdb4f4..4616b55796 100755 --- a/vector/src/main/res/values/config.xml +++ b/vector/src/main/res/values/config.xml @@ -25,6 +25,7 @@ matrix.org + gitter.im diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 7f1d9e1fd3..d82c8bead2 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1596,9 +1596,14 @@ Select a room directory The server may be unavailable or overloaded Type a homeserver to list public rooms from - Homeserver URL + Server name All rooms on %s server All native %s rooms + Your server + Add a new server + Enter the name of a new server you want to explore. + "Can't find this server or its room list" + This server is already present in the list Type here… @@ -3017,11 +3022,11 @@ Sends the given message with confetti - Sends the given message with snow + Sends the given message with snowfall sends confetti 🎉 - sends snow ❄️ + sends snowfall ❄️ Unencrypted Encrypted by an unverified device @@ -3244,7 +3249,9 @@ Transfer An error occurred while transferring call Users - + Consulting with %1$s + Transfer to %1$s + Unknown person Re-Authentication Needed @@ -3274,6 +3281,7 @@ Do not notify View read receipts This room is public + This Space is public Dev Tools Explore Room State @@ -3305,6 +3313,8 @@ Delete unsent messages Are you sure you want to delete all unsent messages in this room? + Public space + Private space Add Space Your public space Your private space @@ -3400,4 +3410,5 @@ Some rooms may be hidden because they’re private and you need an invite.\nYou don’t have permission to add rooms. Some rooms may be hidden because they’re private and you need an invite. + Unnamed Room