diff --git a/CHANGES.md b/CHANGES.md index 628d9759b1..7db26cd8aa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,8 @@ Changes in RiotX 0.13.0 (2020-XX-XX) Features ✨: - Send and render typing events (#564) + - Create Room Profile screen (#54) + - Create Room Member Profile screen (#59) Improvements 🙌: - Render events m.room.encryption and m.room.guest_access in the timeline diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/OptionalRx.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/OptionalRx.kt index d608837d4a..a8503ba428 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/OptionalRx.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/OptionalRx.kt @@ -22,3 +22,9 @@ import io.reactivex.Observable fun Observable>.unwrap(): Observable { return filter { it.hasValue() }.map { it.get() } } + +fun Observable>.mapOptional(fn: (T) -> U?): Observable> { + return map { + it.map(fn) + } +} diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt index bbf0e76823..9491a69ef1 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -16,11 +16,12 @@ package im.vector.matrix.rx +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.ReadReceipt -import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.matrix.android.api.session.room.send.UserDraft @@ -37,7 +38,7 @@ class RxRoom(private val room: Room) { .startWith(room.roomSummary().toOptional()) } - fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable> { + fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable> { return room.getRoomMembersLive(queryParams).asObservable() .startWith(room.getRoomMembers(queryParams)) } @@ -52,6 +53,11 @@ class RxRoom(private val room: Room) { .startWith(room.getTimeLineEvent(eventId).toOptional()) } + fun liveStateEvent(eventType: String): Observable> { + return room.getStateEventLive(eventType).asObservable() + .startWith(room.getStateEvent(eventType).toOptional()) + } + fun liveReadMarker(): Observable> { return room.getReadMarkerLive().asObservable() } diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index 084f497de5..406e274258 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -26,7 +26,9 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional import io.reactivex.Observable import io.reactivex.Single @@ -56,7 +58,8 @@ class RxSession(private val session: Session) { } fun liveUser(userId: String): Observable> { - return session.getUserLive(userId).asObservable().distinctUntilChanged() + return session.getUserLive(userId).asObservable() + .startWith(session.getUser(userId).toOptional()) } fun liveUsers(): Observable> { @@ -91,6 +94,10 @@ class RxSession(private val session: Session) { searchOnServer: Boolean): Single> = singleBuilder { session.getRoomIdByAlias(roomAlias, searchOnServer, it) } + + fun getProfileInfo(userId: String): Single = singleBuilder { + session.getProfile(userId, it) + } } fun Session.rx(): RxSession { diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt index dd4daee9cd..102d9b65d3 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType import kotlin.random.Random @@ -63,7 +63,7 @@ object RoomDataHelper { } fun createFakeRoomMemberEvent(): Event { - val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent() + val roomMember = RoomMemberSummary(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent() return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/SenderNotificationPermissionCondition.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/SenderNotificationPermissionCondition.kt index 46fc9a5c6d..e0d4034082 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/SenderNotificationPermissionCondition.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/pushrules/SenderNotificationPermissionCondition.kt @@ -16,7 +16,8 @@ package im.vector.matrix.android.api.pushrules import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.model.PowerLevels +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper class SenderNotificationPermissionCondition(val key: String) : Condition(Kind.sender_notification_permission) { @@ -28,7 +29,8 @@ class SenderNotificationPermissionCondition(val key: String) : Condition(Kind.se return "User power level <$key>" } - fun isSatisfied(event: Event, powerLevels: PowerLevels): Boolean { - return event.senderId != null && powerLevels.getUserPowerLevel(event.senderId) >= powerLevels.notificationLevel(key) + fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean { + val powerLevelsHelper = PowerLevelsHelper(powerLevels) + return event.senderId != null && powerLevelsHelper.getUserPowerLevel(event.senderId) >= powerLevelsHelper.notificationLevel(key) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 339e6ac4a8..5bd219247c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService +import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService @@ -51,6 +52,7 @@ interface Session : SignOutService, FilterService, FileService, + ProfileService, PushRuleService, PushersService, InitialSyncProgressService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt new file mode 100644 index 0000000000..c1dc9a8afa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/profile/ProfileService.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.matrix.android.api.session.profile + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.api.util.Optional + +/** + * This interface defines methods to handling profile information. It's implemented at the session level. + */ +interface ProfileService { + + companion object Constants { + const val DISPLAY_NAME_KEY = "displayname" + const val AVATAR_URL_KEY = "avatar_url" + } + + /** + * Return the current dispayname for this user + * @param userId the userId param to look for + * + */ + fun getDisplayName(userId: String, matrixCallback: MatrixCallback>): Cancelable + + /** + * Return the current avatarUrl for this user. + * @param userId the userId param to look for + * + */ + fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback>): Cancelable + + /** + * Get the combined profile information for this user. + * This may return keys which are not limited to displayname or avatar_url. + * @param userId the userId param to look for + * + */ + fun getProfile(userId: String, matrixCallback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index 6c117d3be7..98bce9476b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.api.session.room.members import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.util.Cancelable /** @@ -38,21 +38,21 @@ interface MembershipService { * * @return the roomMember with userId or null */ - fun getRoomMember(userId: String): RoomMember? + fun getRoomMember(userId: String): RoomMemberSummary? /** * Return all the roomMembers of the room with params * @param queryParams the params to query for * @return a roomMember list. */ - fun getRoomMembers(queryParams: RoomMemberQueryParams): List + fun getRoomMembers(queryParams: RoomMemberQueryParams): List /** * Return all the roomMembers of the room filtered by memberships * @param queryParams the params to query for * @return a [LiveData] of roomMember list. */ - fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData> + fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData> fun getNumberOfJoinedMembers(): Int diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt index b8ad55213c..b3260c28fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMemberQueryParams.kt @@ -29,11 +29,13 @@ fun roomMemberQueryParams(init: (RoomMemberQueryParams.Builder.() -> Unit) = {}) data class RoomMemberQueryParams( val displayName: QueryStringValue, val memberships: List, + val userId: QueryStringValue, val excludeSelf: Boolean ) { class Builder { + var userId: QueryStringValue = QueryStringValue.NoCondition var displayName: QueryStringValue = QueryStringValue.IsNotEmpty var memberships: List = Membership.all() var excludeSelf: Boolean = false @@ -41,6 +43,7 @@ data class RoomMemberQueryParams( fun build() = RoomMemberQueryParams( displayName = displayName, memberships = memberships, + userId = userId, excludeSelf = excludeSelf ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevels.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevels.kt deleted file mode 100644 index 27f7820156..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevels.kt +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2019 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.matrix.android.api.session.room.model - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import im.vector.matrix.android.api.session.events.model.EventType - -/** - * Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content. - */ -@JsonClass(generateAdapter = true) -data class PowerLevels( - @Json(name = "ban") val ban: Int = 50, - @Json(name = "kick") val kick: Int = 50, - @Json(name = "invite") val invite: Int = 50, - @Json(name = "redact") val redact: Int = 50, - @Json(name = "events_default") val eventsDefault: Int = 0, - @Json(name = "events") val events: MutableMap = HashMap(), - @Json(name = "users_default") val usersDefault: Int = 0, - @Json(name = "users") val users: MutableMap = HashMap(), - @Json(name = "state_default") val stateDefault: Int = 50, - @Json(name = "notifications") val notifications: Map = HashMap() -) { - - /** - * Returns the user power level of a dedicated user Id - * - * @param userId the user id - * @return the power level - */ - fun getUserPowerLevel(userId: String): Int { - return users.getOrElse(userId) { usersDefault } - } - - /** - * Updates the user power levels of a dedicated user id - * - * @param userId the user - * @param powerLevel the new power level - */ - fun setUserPowerLevel(userId: String, powerLevel: Int) { - users[userId] = powerLevel - } - - /** - * Tell if an user can send an event of type 'eventTypeString'. - * - * @param eventTypeString the event type (in Event.EVENT_TYPE_XXX values) - * @param userId the user id - * @return true if the user can send the event - */ - fun maySendEventOfType(eventTypeString: String, userId: String): Boolean { - return if (eventTypeString.isNotEmpty() && userId.isNotEmpty()) { - getUserPowerLevel(userId) >= minimumPowerLevelForSendingEventAsMessage(eventTypeString) - } else false - } - - /** - * Tells if an user can send a room message. - * - * @param userId the user id - * @return true if the user can send a room message - */ - fun maySendMessage(userId: String): Boolean { - return maySendEventOfType(EventType.MESSAGE, userId) - } - - /** - * Helper to get the minimum power level the user must have to send an event of the given type - * as a message. - * - * @param eventTypeString the type of event (in Event.EVENT_TYPE_XXX values) - * @return the required minimum power level. - */ - fun minimumPowerLevelForSendingEventAsMessage(eventTypeString: String?): Int { - return events[eventTypeString] ?: eventsDefault - } - - /** - * Helper to get the minimum power level the user must have to send an event of the given type - * as a state event. - * - * @param eventTypeString the type of event (in Event.EVENT_TYPE_STATE_ values). - * @return the required minimum power level. - */ - fun minimumPowerLevelForSendingEventAsStateEvent(eventTypeString: String?): Int { - return events[eventTypeString] ?: stateDefault - } - - /** - * Get the notification level for a dedicated key. - * - * @param key the notification key - * @return the level - */ - fun notificationLevel(key: String): Int { - val valAsVoid = notifications[key] ?: return 50 - - // the first implementation was a string value - return if (valAsVoid is String) { - valAsVoid.toInt() - } else { - valAsVoid as Int - } - } -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevelsContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevelsContent.kt new file mode 100644 index 0000000000..2f81965d0a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PowerLevelsContent.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 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.matrix.android.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants + +/** + * Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content. + */ +@JsonClass(generateAdapter = true) +data class PowerLevelsContent( + @Json(name = "ban") val ban: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL, + @Json(name = "kick") val kick: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL, + @Json(name = "invite") val invite: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL, + @Json(name = "redact") val redact: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL, + @Json(name = "events_default") val eventsDefault: Int = PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL, + @Json(name = "events") val events: MutableMap = HashMap(), + @Json(name = "users_default") val usersDefault: Int = PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL, + @Json(name = "users") val users: MutableMap = HashMap(), + @Json(name = "state_default") val stateDefault: Int = PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL, + @Json(name = "notifications") val notifications: Map = HashMap() +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberSummary.kt similarity index 96% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberSummary.kt index 994c27be4d..17768362b2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMember.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomMemberSummary.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.model /** * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content */ -data class RoomMember( +data class RoomMemberSummary( val membership: Membership, val userId: String, val displayName: String? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 2f420de164..1f7a7b144a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -32,6 +32,8 @@ data class RoomSummary( val canonicalAlias: String? = null, val aliases: List = emptyList(), val isDirect: Boolean = false, + val joinedMembersCount: Int? = 0, + val invitedMembersCount: Int? = 0, val latestPreviewableEvent: TimelineEvent? = null, val otherMemberIds: List = emptyList(), val notificationCount: Int = 0, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt index 2c192ee8c7..2119c586db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/create/CreateRoomParams.kt @@ -24,7 +24,7 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toContent -import im.vector.matrix.android.api.session.room.model.PowerLevels +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.internal.auth.data.ThreePidMedium @@ -115,7 +115,7 @@ class CreateRoomParams { * The power level content to override in the default power level event */ @Json(name = "power_level_content_override") - var powerLevelContentOverride: PowerLevels? = null + var powerLevelContentOverride: PowerLevelsContent? = null /** * Add the crypto algorithm to the room creation parameters. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsConstants.kt new file mode 100644 index 0000000000..16af3d216e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsConstants.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.matrix.android.api.session.room.powerlevels + +object PowerLevelsConstants { + + const val DEFAULT_ROOM_ADMIN_LEVEL = 100 + const val DEFAULT_ROOM_MODERATOR_LEVEL = 50 + const val DEFAULT_ROOM_USER_LEVEL = 0 +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt new file mode 100644 index 0000000000..b18a7dd97b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/powerlevels/PowerLevelsHelper.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.matrix.android.api.session.room.powerlevels + +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent + +/** + * This class is an helper around PowerLevelsContent. + */ +class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) { + + /** + * Returns the user power level of a dedicated user Id + * + * @param userId the user id + * @return the power level + */ + fun getUserPowerLevel(userId: String): Int { + return powerLevelsContent.users.getOrElse(userId) { + powerLevelsContent.usersDefault + } + } + + /** + * Tell if an user can send an event of a certain type + * + * @param eventType the event type to check for + * @param userId the user id + * @return true if the user can send this type of event + */ + fun isAllowedToSend(eventType: String, userId: String): Boolean { + return if (eventType.isNotEmpty() && userId.isNotEmpty()) { + val powerLevel = getUserPowerLevel(userId) + val minimumPowerLevel = powerLevelsContent.events[eventType] + ?: if (EventType.isStateEvent(eventType)) { + powerLevelsContent.stateDefault + } else { + powerLevelsContent.eventsDefault + } + powerLevel >= minimumPowerLevel + } else false + } + + /** + * Get the notification level for a dedicated key. + * + * @param key the notification key + * @return the level + */ + fun notificationLevel(key: String): Int { + return when (val value = powerLevelsContent.notifications[key]) { + // the first implementation was a string value + is String -> value.toInt() + is Int -> value + else -> PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt index 3372eb874c..55bf816b7a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/state/StateService.kt @@ -16,8 +16,10 @@ package im.vector.matrix.android.api.session.room.state +import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.util.Optional interface StateService { @@ -32,4 +34,6 @@ interface StateService { fun enableEncryption(algorithm: String, callback: MatrixCallback) fun getStateEvent(eventType: String): Event? + + fun getStateEventLive(eventType: String): LiveData> } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt index d6ef522f41..5b3ca234ac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt @@ -18,7 +18,7 @@ package im.vector.matrix.android.api.util import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.session.group.model.GroupSummary -import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.user.model.User @@ -147,4 +147,4 @@ fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, ava fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl) fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlias ?: roomId, displayName, avatarUrl) fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl) -fun RoomMember.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) +fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt index 2f602d0b84..a0f677d96d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Optional.kt @@ -27,6 +27,14 @@ data class Optional constructor(private val value: T?) { return value } + fun map(fn: (T) -> U?): Optional { + return if (value == null) { + from(null) + } else { + from(fn(value)) + } + } + fun getOrElse(fn: () -> T): T { return value ?: fn() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index d0267e3345..783b7eb4ad 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -38,7 +38,7 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent -import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting @@ -64,7 +64,7 @@ import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask -import im.vector.matrix.android.internal.session.room.membership.RoomMembers +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -697,9 +697,9 @@ internal class DefaultCryptoService @Inject constructor( && shouldEncryptForInvitedMembers(roomId) userIds = if (encryptForInvitedMembers) { - RoomMembers(realm, roomId).getActiveRoomMemberIds() + RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() } else { - RoomMembers(realm, roomId).getJoinedRoomMemberIds() + RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds() } } return userIds @@ -722,7 +722,7 @@ internal class DefaultCryptoService @Inject constructor( return } event.stateKey?.let { userId -> - val roomMember: RoomMember? = event.content.toModel() + val roomMember: RoomMemberSummary? = event.content.toModel() val membership = roomMember?.membership if (membership == Membership.JOIN) { // make sure we are tracking the deviceList for this user. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt index 19c4715faa..b6c6c6c1ac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt @@ -24,7 +24,7 @@ import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.fastContains import im.vector.matrix.android.internal.extensions.assertIsManaged -import im.vector.matrix.android.internal.session.room.membership.RoomMembers +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) { chunks.remove(chunkEntity) @@ -59,7 +59,7 @@ internal fun RoomEntity.addSendingEvent(event: Event) { val eventEntity = event.toEntity(roomId).apply { this.sendState = SendState.UNSENT } - val roomMembers = RoomMembers(realm, roomId) + val roomMembers = RoomMemberHelper(realm, roomId) val myUser = roomMembers.getLastRoomMember(senderId) val localId = TimelineEventEntity.nextId(realm) val timelineEventEntity = TimelineEventEntity(localId).also { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt index 983de3a50f..17103ce337 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/TimelineEventSenderVisitor.kt @@ -26,7 +26,7 @@ import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.extensions.assertIsManaged import im.vector.matrix.android.internal.session.SessionScope -import im.vector.matrix.android.internal.session.room.membership.RoomMembers +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import io.realm.RealmList import io.realm.RealmQuery import javax.inject.Inject @@ -128,7 +128,7 @@ internal class TimelineEventSenderVisitor @Inject constructor() { ContentMapper.map(senderRoomMemberContent).toModel()?.also { result.senderAvatar = it.avatarUrl result.senderName = it.displayName - result.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) + result.isUniqueDisplayName = RoomMemberHelper(realm, roomId).isUniqueDisplayName(it.displayName) } // We try to fallback on prev content if we got a room member state events with null fields if (root?.type == EventType.STATE_ROOM_MEMBER) { @@ -138,7 +138,7 @@ internal class TimelineEventSenderVisitor @Inject constructor() { } if (result.senderName == null && it.displayName != null) { result.senderName = it.displayName - result.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName) + result.isUniqueDisplayName = RoomMemberHelper(realm, roomId).isUniqueDisplayName(it.displayName) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberSummaryMapper.kt similarity index 59% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberSummaryMapper.kt index a458c5e506..470772a40e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomMemberSummaryMapper.kt @@ -16,21 +16,21 @@ package im.vector.matrix.android.internal.database.mapper -import im.vector.matrix.android.api.session.room.model.RoomMember -import im.vector.matrix.android.internal.database.model.RoomMemberEntity +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity -internal object RoomMemberMapper { +internal object RoomMemberSummaryMapper { - fun map(roomMemberEntity: RoomMemberEntity): RoomMember { - return RoomMember( - userId = roomMemberEntity.userId, - avatarUrl = roomMemberEntity.avatarUrl, - displayName = roomMemberEntity.displayName, - membership = roomMemberEntity.membership + fun map(roomMemberSummaryEntity: RoomMemberSummaryEntity): RoomMemberSummary { + return RoomMemberSummary( + userId = roomMemberSummaryEntity.userId, + avatarUrl = roomMemberSummaryEntity.avatarUrl, + displayName = roomMemberSummaryEntity.displayName, + membership = roomMemberSummaryEntity.membership ) } } -internal fun RoomMemberEntity.asDomain(): RoomMember { - return RoomMemberMapper.map(this) +internal fun RoomMemberSummaryEntity.asDomain(): RoomMemberSummary { + return RoomMemberSummaryMapper.map(this) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 3d1b5a2d08..72d221aafe 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -60,6 +60,8 @@ internal class RoomSummaryMapper @Inject constructor( avatarUrl = roomSummaryEntity.avatarUrl ?: "", isDirect = roomSummaryEntity.isDirect, latestPreviewableEvent = latestEvent, + joinedMembersCount = roomSummaryEntity.joinedMembersCount, + invitedMembersCount = roomSummaryEntity.invitedMembersCount, otherMemberIds = roomSummaryEntity.otherMemberIds.toList(), highlightCount = roomSummaryEntity.highlightCount, notificationCount = roomSummaryEntity.notificationCount, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberSummaryEntity.kt similarity index 67% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberSummaryEntity.kt index c532857fe1..45bf1b3a22 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomMemberSummaryEntity.kt @@ -21,13 +21,13 @@ import io.realm.RealmObject import io.realm.annotations.Index import io.realm.annotations.PrimaryKey -internal open class RoomMemberEntity(@PrimaryKey var primaryKey: String = "", - @Index var userId: String = "", - @Index var roomId: String = "", - var displayName: String = "", - var avatarUrl: String = "", - var reason: String? = null, - var isDirect: Boolean = false +internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "", + @Index var userId: String = "", + @Index var roomId: String = "", + var displayName: String? = null, + var avatarUrl: String? = null, + var reason: String? = null, + var isDirect: Boolean = false ) : RealmObject() { private var membershipStr: String = Membership.NONE.name diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 07ff1df005..a29e7e89fb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -50,6 +50,6 @@ import io.realm.annotations.RealmModule UserDraftsEntity::class, DraftEntity::class, HomeServerCapabilitiesEntity::class, - RoomMemberEntity::class + RoomMemberSummaryEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 62f53c3d7b..f3f8db0ea0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -60,20 +60,7 @@ internal fun EventEntity.Companion.types(realm: Realm, return query } -internal fun RealmQuery.next(from: Int? = null, strict: Boolean = true): EventEntity? { - if (from != null) { - if (strict) { - this.greaterThan(EventEntityFields.STATE_INDEX, from) - } else { - this.greaterThanOrEqualTo(EventEntityFields.STATE_INDEX, from) - } - } - return this - .sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING) - .findFirst() -} - -internal fun RealmQuery.prev(since: Int? = null, strict: Boolean = false): EventEntity? { +internal fun RealmQuery.descending(since: Int? = null, strict: Boolean = false): RealmQuery { if (since != null) { if (strict) { this.lessThan(EventEntityFields.STATE_INDEX, since) @@ -81,9 +68,26 @@ internal fun RealmQuery.prev(since: Int? = null, strict: Boolean = this.lessThanOrEqualTo(EventEntityFields.STATE_INDEX, since) } } - return this - .sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) - .findFirst() + return this.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING) +} + +internal fun RealmQuery.ascending(from: Int? = null, strict: Boolean = true): RealmQuery { + if (from != null) { + if (strict) { + this.greaterThan(EventEntityFields.STATE_INDEX, from) + } else { + this.greaterThanOrEqualTo(EventEntityFields.STATE_INDEX, from) + } + } + return this.sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING) +} + +internal fun RealmQuery.next(from: Int? = null, strict: Boolean = true): EventEntity? { + return this.ascending(from, strict).findFirst() +} + +internal fun RealmQuery.prev(since: Int? = null, strict: Boolean = false): EventEntity? { + return descending(since, strict).findFirst() } internal fun RealmList.find(eventId: String): EventEntity? { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt index 2ddade0048..e2dd2a7d8c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomMemberEntityQueries.kt @@ -16,19 +16,19 @@ package im.vector.matrix.android.internal.database.query -import im.vector.matrix.android.internal.database.model.RoomMemberEntity -import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.kotlin.where -internal fun RoomMemberEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery { +internal fun RoomMemberSummaryEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery { val query = realm - .where() - .equalTo(RoomMemberEntityFields.ROOM_ID, roomId) + .where() + .equalTo(RoomMemberSummaryEntityFields.ROOM_ID, roomId) if (userId != null) { - query.equalTo(RoomMemberEntityFields.USER_ID, userId) + query.equalTo(RoomMemberSummaryEntityFields.USER_ID, userId) } return query } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index dc1a20802b..e22e47bc1c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -33,6 +33,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService +import im.vector.matrix.android.api.session.profile.ProfileService import im.vector.matrix.android.api.session.pushers.PushersService import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService @@ -80,6 +81,7 @@ internal class DefaultSession @Inject constructor( private val cryptoService: Lazy, private val fileService: Lazy, private val secureStorageService: Lazy, + private val profileService: Lazy, private val syncThreadProvider: Provider, private val contentUrlResolver: ContentUrlResolver, private val syncTokenStore: SyncTokenStore, @@ -101,7 +103,8 @@ internal class DefaultSession @Inject constructor( FileService by fileService.get(), InitialSyncProgressService by initialSyncProgressService.get(), SecureStorageService by secureStorageService.get(), - HomeServerCapabilitiesService by homeServerCapabilitiesService.get() { + HomeServerCapabilitiesService by homeServerCapabilitiesService.get(), + ProfileService by profileService.get() { private var isOpen = false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index a208a7a720..5b6aa57979 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.session.filter.FilterModule import im.vector.matrix.android.internal.session.group.GetGroupDataWorker import im.vector.matrix.android.internal.session.group.GroupModule import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule +import im.vector.matrix.android.internal.session.profile.ProfileModule import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker import im.vector.matrix.android.internal.session.pushers.PushersModule import im.vector.matrix.android.internal.session.room.RoomModule @@ -64,6 +65,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers CryptoModule::class, PushersModule::class, AccountDataModule::class, + ProfileModule::class, SessionAssistedInjectModule::class ] ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt new file mode 100644 index 0000000000..e2c18e41d6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/DefaultProfileService.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.matrix.android.internal.session.profile + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.profile.ProfileService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import javax.inject.Inject + +internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor, + private val getProfileInfoTask: GetProfileInfoTask) : ProfileService { + + override fun getDisplayName(userId: String, matrixCallback: MatrixCallback>): Cancelable { + val params = GetProfileInfoTask.Params(userId) + return getProfileInfoTask + .configureWith(params) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: JsonDict) { + val displayName = data[ProfileService.DISPLAY_NAME_KEY] as? String + matrixCallback.onSuccess(Optional.from(displayName)) + } + + override fun onFailure(failure: Throwable) { + matrixCallback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback>): Cancelable { + val params = GetProfileInfoTask.Params(userId) + return getProfileInfoTask + .configureWith(params) { + this.callback = object : MatrixCallback { + override fun onSuccess(data: JsonDict) { + val avatarUrl = data[ProfileService.AVATAR_URL_KEY] as? String + matrixCallback.onSuccess(Optional.from(avatarUrl)) + } + + override fun onFailure(failure: Throwable) { + matrixCallback.onFailure(failure) + } + } + } + .executeBy(taskExecutor) + } + + override fun getProfile(userId: String, matrixCallback: MatrixCallback): Cancelable { + val params = GetProfileInfoTask.Params(userId) + return getProfileInfoTask + .configureWith(params) { + this.callback = matrixCallback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/GetProfileInfoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/GetProfileInfoTask.kt new file mode 100644 index 0000000000..f51092434e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/GetProfileInfoTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.matrix.android.internal.session.profile + +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal abstract class GetProfileInfoTask : Task { + data class Params( + val userId: String + ) +} + +internal class DefaultGetProfileInfoTask @Inject constructor(private val profileAPI: ProfileAPI, + private val eventBus: EventBus) : GetProfileInfoTask() { + + override suspend fun execute(params: Params): JsonDict { + return executeRequest(eventBus) { + apiCall = profileAPI.getProfile(params.userId) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt new file mode 100644 index 0000000000..197d85f879 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileAPI.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.matrix.android.internal.session.profile + +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +interface ProfileAPI { + + /** + * Get the combined profile information for this user. + * This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers. + * This API may return keys which are not limited to displayname or avatar_url. + * @param userId the user id to fetch profile info + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}") + fun getProfile(@Path("userId") userId: String): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt new file mode 100644 index 0000000000..7005a5341f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/profile/ProfileModule.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.matrix.android.internal.session.profile + +import dagger.Binds +import dagger.Module +import dagger.Provides +import im.vector.matrix.android.api.session.profile.ProfileService +import im.vector.matrix.android.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class ProfileModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesProfileAPI(retrofit: Retrofit): ProfileAPI { + return retrofit.create(ProfileAPI::class.java) + } + } + + @Binds + abstract fun bindProfileService(service: DefaultProfileService): ProfileService + + @Binds + abstract fun bindGetProfileTask(task: DefaultGetProfileInfoTask): GetProfileInfoTask +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt index 0bb2dc0f27..2893bf5126 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt @@ -22,11 +22,11 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomAvatarContent import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.UserId -import im.vector.matrix.android.internal.session.room.membership.RoomMembers +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import javax.inject.Inject internal class RoomAvatarResolver @Inject constructor(private val monarchy: Monarchy, @@ -45,13 +45,13 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona if (!res.isNullOrEmpty()) { return@doWithRealm } - val roomMembers = RoomMembers(realm, roomId) + val roomMembers = RoomMemberHelper(realm, roomId) val members = roomMembers.queryActiveRoomMembersEvent().findAll() // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) if (members.size == 1) { res = members.firstOrNull()?.avatarUrl } else if (members.size == 2) { - val firstOtherMember = members.where().notEqualTo(RoomMemberEntityFields.USER_ID, userId).findFirst() + val firstOtherMember = members.where().notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId).findFirst() res = firstOtherMember?.avatarUrl } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 6d4dac64b5..bbb5feba15 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -24,14 +24,14 @@ import im.vector.matrix.android.api.session.room.model.RoomAliasesContent import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.* import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver -import im.vector.matrix.android.internal.session.room.membership.RoomMembers +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.sync.RoomSyncHandler import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications @@ -115,9 +115,9 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.typingUserIds.addAll(ephemeralResult?.typingUserIds.orEmpty()) if (updateMembers) { - val otherRoomMembers = RoomMembers(realm, roomId) + val otherRoomMembers = RoomMemberHelper(realm, roomId) .queryRoomMembersEvent() - .notEqualTo(RoomMemberEntityFields.USER_ID, userId) + .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) .findAll() .asSequence() .map { it.userId } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index dc44359666..d17614ca4e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -24,11 +24,11 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.database.model.RoomMemberEntity -import im.vector.matrix.android.internal.database.model.RoomMemberEntityFields +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.query.process import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask @@ -66,14 +66,14 @@ internal class DefaultMembershipService @AssistedInject constructor( .executeBy(taskExecutor) } - override fun getRoomMember(userId: String): RoomMember? { + override fun getRoomMember(userId: String): RoomMemberSummary? { val roomMemberEntity = monarchy.fetchCopied { - RoomMembers(it, roomId).getLastRoomMember(userId) + RoomMemberHelper(it, roomId).getLastRoomMember(userId) } return roomMemberEntity?.asDomain() } - override fun getRoomMembers(queryParams: RoomMemberQueryParams): List { + override fun getRoomMembers(queryParams: RoomMemberQueryParams): List { return monarchy.fetchAllMappedSync( { roomMembersQuery(it, queryParams) @@ -84,7 +84,7 @@ internal class DefaultMembershipService @AssistedInject constructor( ) } - override fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData> { + override fun getRoomMembersLive(queryParams: RoomMemberQueryParams): LiveData> { return monarchy.findAllMappedWithChanges( { roomMembersQuery(it, queryParams) @@ -95,20 +95,21 @@ internal class DefaultMembershipService @AssistedInject constructor( ) } - private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery { - return RoomMembers(realm, roomId).queryRoomMembersEvent() - .process(RoomMemberEntityFields.MEMBERSHIP_STR, queryParams.memberships) - .process(RoomMemberEntityFields.DISPLAY_NAME, queryParams.displayName) + private fun roomMembersQuery(realm: Realm, queryParams: RoomMemberQueryParams): RealmQuery { + return RoomMemberHelper(realm, roomId).queryRoomMembersEvent() + .process(RoomMemberSummaryEntityFields.USER_ID, queryParams.userId) + .process(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships) + .process(RoomMemberSummaryEntityFields.DISPLAY_NAME, queryParams.displayName) .apply { if (queryParams.excludeSelf) { - notEqualTo(RoomMemberEntityFields.USER_ID, userId) + notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) } } } override fun getNumberOfJoinedMembers(): Int { return Realm.getInstance(monarchy.realmConfiguration).use { - RoomMembers(it, roomId).getNumberOfJoinedMembers() + RoomMemberHelper(it, roomId).getNumberOfJoinedMembers() } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index 9382fbc54a..67b222ecb8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -26,7 +26,7 @@ import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.model.RoomMemberEntity +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where @@ -75,7 +75,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return@doWithRealm } - val roomMembers = RoomMembers(realm, roomId) + val roomMembers = RoomMemberHelper(realm, roomId) val activeMembers = roomMembers.queryActiveRoomMembersEvent().findAll() if (roomEntity?.membership == Membership.INVITE) { @@ -83,7 +83,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: val inviterId = inviteMeEvent?.sender name = if (inviterId != null) { activeMembers.where() - .equalTo(RoomMemberEntityFields.USER_ID, inviterId) + .equalTo(RoomMemberSummaryEntityFields.USER_ID, inviterId) .findFirst() ?.displayName } else { @@ -91,7 +91,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: } } else if (roomEntity?.membership == Membership.JOIN) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() - val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { + val otherMembersSubset: List = if (roomSummary?.heroes?.isNotEmpty() == true) { roomSummary.heroes.mapNotNull { userId -> roomMembers.getLastRoomMember(userId)?.takeIf { it.membership == Membership.INVITE || it.membership == Membership.JOIN @@ -99,7 +99,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: } } else { activeMembers.where() - .notEqualTo(RoomMemberEntityFields.USER_ID, userId) + .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) .limit(3) .findAll() .createSnapshot() @@ -123,14 +123,14 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: return name ?: roomId } - private fun resolveRoomMemberName(roomMember: RoomMemberEntity?, - roomMembers: RoomMembers): String? { - if (roomMember == null) return null - val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName) + private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?, + roomMemberHelper: RoomMemberHelper): String? { + if (roomMemberSummary == null) return null + val isUnique = roomMemberHelper.isUniqueDisplayName(roomMemberSummary.displayName) return if (isUnique) { - roomMember.displayName + roomMemberSummary.displayName } else { - "${roomMember.displayName} (${roomMember.userId})" + "${roomMemberSummary.displayName} (${roomMemberSummary.userId})" } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt index 51df244401..291d2af3a6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberEntityFactory.kt @@ -17,18 +17,18 @@ package im.vector.matrix.android.internal.session.room.membership import im.vector.matrix.android.api.session.room.model.RoomMemberContent -import im.vector.matrix.android.internal.database.model.RoomMemberEntity +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity internal object RoomMemberEntityFactory { - fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberEntity { + fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberSummaryEntity { val primaryKey = "${roomId}_$userId" - return RoomMemberEntity( + return RoomMemberSummaryEntity( primaryKey = primaryKey, userId = userId, roomId = roomId, - displayName = roomMember.displayName ?: "", - avatarUrl = roomMember.avatarUrl ?: "" + displayName = roomMember.displayName, + avatarUrl = roomMember.avatarUrl ).apply { membership = roomMember.membership } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberHelper.kt similarity index 78% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberHelper.kt index e3775f5ade..029afcbe40 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberHelper.kt @@ -20,7 +20,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.RoomMemberEntity +import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.where import io.realm.Realm @@ -32,8 +32,8 @@ import io.realm.Sort * It allows to get the live membership of a user. */ -internal class RoomMembers(private val realm: Realm, - private val roomId: String +internal class RoomMemberHelper(private val realm: Realm, + private val roomId: String ) { private val roomSummary: RoomSummaryEntity? by lazy { @@ -48,8 +48,8 @@ internal class RoomMembers(private val realm: Realm, .findFirst() } - fun getLastRoomMember(userId: String): RoomMemberEntity? { - return RoomMemberEntity + fun getLastRoomMember(userId: String): RoomMemberSummaryEntity? { + return RoomMemberSummaryEntity .where(realm, roomId, userId) .findFirst() } @@ -66,26 +66,26 @@ internal class RoomMembers(private val realm: Realm, .size == 1 } - fun queryRoomMembersEvent(): RealmQuery { - return RoomMemberEntity.where(realm, roomId) + fun queryRoomMembersEvent(): RealmQuery { + return RoomMemberSummaryEntity.where(realm, roomId) } - fun queryJoinedRoomMembersEvent(): RealmQuery { + fun queryJoinedRoomMembersEvent(): RealmQuery { return queryRoomMembersEvent() - .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) } - fun queryInvitedRoomMembersEvent(): RealmQuery { + fun queryInvitedRoomMembersEvent(): RealmQuery { return queryRoomMembersEvent() - .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) } - fun queryActiveRoomMembersEvent(): RealmQuery { + fun queryActiveRoomMembersEvent(): RealmQuery { return queryRoomMembersEvent() .beginGroup() - .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.INVITE.name) .or() - .equalTo(RoomMemberEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) + .equalTo(RoomMemberSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) .endGroup() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index 3cd73a97e0..eb7208ea0d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -16,27 +16,30 @@ package im.vector.matrix.android.internal.session.room.state +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.state.StateService +import im.vector.matrix.android.api.util.Optional +import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.query.descending import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import io.realm.Realm -import io.realm.RealmConfiguration import java.security.InvalidParameterException internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String, - @SessionDatabase - private val realmConfiguration: RealmConfiguration, + private val monarchy: Monarchy, private val taskExecutor: TaskExecutor, private val sendStateTask: SendStateTask ) : StateService { @@ -47,11 +50,21 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private } override fun getStateEvent(eventType: String): Event? { - return Realm.getInstance(realmConfiguration).use { realm -> + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> EventEntity.where(realm, roomId, eventType).prev()?.asDomain() } } + override fun getStateEventLive(eventType: String): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm -> EventEntity.where(realm, roomId, eventType).descending() }, + { it.asDomain() } + ) + return Transformations.map(liveData) { results -> + results.firstOrNull().toOptional() + } + } + override fun updateTopic(topic: String, callback: MatrixCallback) { val params = SendStateTask.Params(roomId, EventType.STATE_ROOM_TOPIC, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt index 139a3297cd..07e8664102 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt @@ -28,7 +28,7 @@ import im.vector.matrix.android.internal.database.query.getDirectRooms import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.UserId -import im.vector.matrix.android.internal.session.room.membership.RoomMembers +import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync import im.vector.matrix.android.internal.session.sync.model.accountdata.* import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper @@ -70,7 +70,7 @@ internal class UserAccountDataSyncHandler @Inject constructor( var hasUpdate = false monarchy.doWithRealm { realm -> invites.forEach { (roomId, _) -> - val myUserStateEvent = RoomMembers(realm, roomId).getLastStateEvent(userId) + val myUserStateEvent = RoomMemberHelper(realm, roomId).getLastStateEvent(userId) val inviterId = myUserStateEvent?.sender val myUserRoomMember: RoomMemberContent? = myUserStateEvent?.let { it.asDomain().content?.toModel() } val isDirect = myUserRoomMember?.isDirect diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 308ca60094..a0d4402767 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -75,7 +75,7 @@ android:value=".features.home.HomeActivity" /> - + @@ -106,6 +106,9 @@ + + + + + + + + = appBarLayout.totalScrollRange) { + if (currentState != State.COLLAPSED) { + onStateChanged(appBarLayout, State.COLLAPSED) + } + State.COLLAPSED + } else { + if (currentState != State.IDLE) { + onStateChanged(appBarLayout, State.IDLE) + } + State.IDLE + } + } + + abstract fun onStateChanged(appBarLayout: AppBarLayout, state: State) +} diff --git a/vector/src/main/java/im/vector/riotx/core/animations/MatrixItemAppBarStateChangeListener.kt b/vector/src/main/java/im/vector/riotx/core/animations/MatrixItemAppBarStateChangeListener.kt new file mode 100644 index 0000000000..427f37ba49 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/animations/MatrixItemAppBarStateChangeListener.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.core.animations + +import android.view.View +import com.google.android.material.appbar.AppBarLayout + +class MatrixItemAppBarStateChangeListener(private val headerView: View, private val toolbarViews: List) : AppBarStateChangeListener() { + + override fun onStateChanged(appBarLayout: AppBarLayout, state: State) { + if (state == State.COLLAPSED) { + headerView.visibility = View.INVISIBLE + toolbarViews.forEach { + it.animate().alpha(1f).duration = 150 + } + } else { + headerView.visibility = View.VISIBLE + toolbarViews.forEach { + it.animate().alpha(0f).duration = 150 + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt b/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt new file mode 100644 index 0000000000..967d7d638d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/animations/behavior/PercentViewBehavior.kt @@ -0,0 +1,221 @@ +/* + * Copyright 2019 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.riotx.core.animations.behavior + +import android.animation.ArgbEvaluator +import android.content.Context +import android.graphics.drawable.ColorDrawable +import android.util.AttributeSet +import android.view.View +import androidx.coordinatorlayout.widget.CoordinatorLayout + +import im.vector.riotx.R +import kotlin.math.abs + +private const val UNSPECIFIED_INT = Integer.MAX_VALUE +private val UNSPECIFIED_FLOAT = Float.MAX_VALUE +private const val DEPEND_TYPE_HEIGHT = 0 +private const val DEPEND_TYPE_WIDTH = 1 +private const val DEPEND_TYPE_X = 2 +private const val DEPEND_TYPE_Y = 3 + +class PercentViewBehavior(context: Context, attrs: AttributeSet) : CoordinatorLayout.Behavior(context, attrs) { + + private var dependType: Int = 0 + private var dependViewId: Int = 0 + private var dependTarget: Int = 0 + private var dependStartX: Int = 0 + private var dependStartY: Int = 0 + private var dependStartWidth: Int = 0 + private var dependStartHeight: Int = 0 + + private var startX: Int = 0 + private var startY: Int = 0 + private var startWidth: Int = 0 + private var startHeight: Int = 0 + private var startBackgroundColor: Int = 0 + private var startAlpha: Float = 0f + private var startRotateX: Float = 0f + private var startRotateY: Float = 0f + + private var targetX: Int = 0 + private var targetY: Int = 0 + private var targetWidth: Int = 0 + private var targetHeight: Int = 0 + private var targetBackgroundColor: Int = 0 + private var targetAlpha: Float = 0f + private var targetRotateX: Float = 0f + private var targetRotateY: Float = 0f + + /** + * Is the values prepared to be use + */ + private var isPrepared: Boolean = false + + init { + val a = context.obtainStyledAttributes(attrs, R.styleable.PercentViewBehavior) + dependViewId = a.getResourceId(R.styleable.PercentViewBehavior_behavior_dependsOn, 0) + dependType = a.getInt(R.styleable.PercentViewBehavior_behavior_dependType, DEPEND_TYPE_WIDTH) + dependTarget = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_dependTarget, UNSPECIFIED_INT) + targetX = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetX, UNSPECIFIED_INT) + targetY = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetY, UNSPECIFIED_INT) + targetWidth = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetWidth, UNSPECIFIED_INT) + targetHeight = a.getDimensionPixelOffset(R.styleable.PercentViewBehavior_behavior_targetHeight, UNSPECIFIED_INT) + targetBackgroundColor = a.getColor(R.styleable.PercentViewBehavior_behavior_targetBackgroundColor, UNSPECIFIED_INT) + targetAlpha = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetAlpha, UNSPECIFIED_FLOAT) + targetRotateX = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateX, UNSPECIFIED_FLOAT) + targetRotateY = a.getFloat(R.styleable.PercentViewBehavior_behavior_targetRotateY, UNSPECIFIED_FLOAT) + a.recycle() + } + + private fun prepare(parent: CoordinatorLayout, child: View, dependency: View) { + dependStartX = dependency.x.toInt() + dependStartY = dependency.y.toInt() + dependStartWidth = dependency.width + dependStartHeight = dependency.height + startX = child.x.toInt() + startY = child.y.toInt() + startWidth = child.width + startHeight = child.height + startAlpha = child.alpha + startRotateX = child.rotationX + startRotateY = child.rotationY + + // only set the start background color when the background is color drawable + val background = child.background + if (background is ColorDrawable) { + startBackgroundColor = background.color + } + + // if parent fitsSystemWindows is true, add status bar height to target y if specified + if (parent.fitsSystemWindows && targetY != UNSPECIFIED_INT) { + var result = 0 + val resources = parent.context.resources + val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") + if (resourceId > 0) { + result = resources.getDimensionPixelSize(resourceId) + } + targetY += result + } + isPrepared = true + } + + override fun layoutDependsOn(parent: CoordinatorLayout, child: V, dependency: View): Boolean { + return dependency.id == dependViewId + } + + override fun onDependentViewChanged(parent: CoordinatorLayout, child: V, dependency: View): Boolean { + // first time, prepare values before continue + if (!isPrepared) { + prepare(parent, child, dependency) + } + updateView(child, dependency) + return false + } + + override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean { + val bool = super.onLayoutChild(parent, child, layoutDirection) + if (isPrepared) { + updateView(child, parent.getDependencies(child)[0]) + } + return bool + } + + /** + * Update the child view from the dependency states + * + * @param child child view + * @param dependency dependency view + */ + private fun updateView(child: V, dependency: View) { + var percent = 0f + var start = 0f + var current = 0f + var end = UNSPECIFIED_INT.toFloat() + when (dependType) { + DEPEND_TYPE_WIDTH -> { + start = dependStartWidth.toFloat() + current = dependency.width.toFloat() + end = dependTarget.toFloat() + } + DEPEND_TYPE_HEIGHT -> { + start = dependStartHeight.toFloat() + current = dependency.height.toFloat() + end = dependTarget.toFloat() + } + DEPEND_TYPE_X -> { + start = dependStartX.toFloat() + current = dependency.x + end = dependTarget.toFloat() + } + DEPEND_TYPE_Y -> { + start = dependStartY.toFloat() + current = dependency.y + end = dependTarget.toFloat() + } + } + + // need to define target value according to the depend type, if not then skip + if (end != UNSPECIFIED_INT.toFloat()) { + percent = abs(current - start) / abs(end - start) + } + updateViewWithPercent(child, if (percent > 1f) 1f else percent) + } + + private fun updateViewWithPercent(child: View, percent: Float) { + var newX = if (targetX == UNSPECIFIED_INT) 0f else (targetX - startX) * percent + var newY = if (targetY == UNSPECIFIED_INT) 0f else (targetY - startY) * percent + + // set scale + if (targetWidth != UNSPECIFIED_INT) { + val newWidth = startWidth + (targetWidth - startWidth) * percent + child.scaleX = newWidth / startWidth + newX -= (startWidth - newWidth) / 2 + } + if (targetHeight != UNSPECIFIED_INT) { + val newHeight = startHeight + (targetHeight - startHeight) * percent + child.scaleY = newHeight / startHeight + newY -= (startHeight - newHeight) / 2 + } + + // set new position + child.translationX = newX + child.translationY = newY + + // set alpha + if (targetAlpha != UNSPECIFIED_FLOAT) { + child.alpha = startAlpha + (targetAlpha - startAlpha) * percent + } + + // set background color + if (targetBackgroundColor != UNSPECIFIED_INT && startBackgroundColor != 0) { + val evaluator = ArgbEvaluator() + val color = evaluator.evaluate(percent, startBackgroundColor, targetBackgroundColor) as Int + child.setBackgroundColor(color) + } + + // set rotation + if (targetRotateX != UNSPECIFIED_FLOAT) { + child.rotationX = startRotateX + (targetRotateX - startRotateX) * percent + } + if (targetRotateY != UNSPECIFIED_FLOAT) { + child.rotationY = startRotateY + (targetRotateY - startRotateY) * percent + } + + child.requestLayout() + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 5565369006..b99268d6d4 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -30,20 +30,23 @@ import im.vector.riotx.features.crypto.verification.SASVerificationVerifiedFragm import im.vector.riotx.features.home.HomeDetailFragment import im.vector.riotx.features.home.HomeDrawerFragment import im.vector.riotx.features.home.LoadingFragment -import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsersFragment -import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment -import im.vector.riotx.features.home.group.GroupListFragment +import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment +import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment +import im.vector.riotx.features.grouplist.GroupListFragment import im.vector.riotx.features.home.room.breadcrumbs.BreadcrumbsFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.login.* import im.vector.riotx.features.login.terms.LoginTermsFragment +import im.vector.riotx.features.roommemberprofile.RoomMemberProfileFragment import im.vector.riotx.features.reactions.EmojiChooserFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment +import im.vector.riotx.features.roomprofile.RoomProfileFragment +import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment import im.vector.riotx.features.settings.* import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment @@ -259,6 +262,21 @@ interface FragmentModule { @FragmentKey(PublicRoomsFragment::class) fun bindPublicRoomsFragment(fragment: PublicRoomsFragment): Fragment + @Binds + @IntoMap + @FragmentKey(RoomProfileFragment::class) + fun bindRoomProfileFragment(fragment: RoomProfileFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(RoomMemberListFragment::class) + fun bindRoomMemberListFragment(fragment: RoomMemberListFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(RoomMemberProfileFragment::class) + fun bindRoomMemberProfileFragment(fragment: RoomMemberProfileFragment): Fragment + @Binds @IntoMap @FragmentKey(BreadcrumbsFragment::class) diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index aec3372098..2463b577a6 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -27,7 +27,7 @@ import im.vector.riotx.features.MainActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeModule -import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity +import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 283b43a004..f553513bfa 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -36,7 +36,7 @@ import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.HomeRoomListDataSource -import im.vector.riotx.features.home.group.SelectedGroupDataSource +import im.vector.riotx.features.grouplist.SelectedGroupDataSource import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.VectorHtmlCompressor import im.vector.riotx.features.navigation.Navigator diff --git a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt index 4b136c557b..5d4288f4b8 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ViewModelModule.kt @@ -22,19 +22,20 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import im.vector.riotx.core.platform.ConfigurationViewModel +import im.vector.riotx.features.createdirect.CreateDirectRoomSharedActionViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel import im.vector.riotx.features.crypto.verification.SasVerificationViewModel import im.vector.riotx.features.home.HomeSharedActionViewModel -import im.vector.riotx.features.home.createdirect.CreateDirectRoomSharedActionViewModel import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.riotx.features.login.LoginSharedActionViewModel import im.vector.riotx.features.reactions.EmojiChooserViewModel import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel +import im.vector.riotx.features.roomprofile.RoomProfileSharedActionViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel @Module @@ -124,4 +125,9 @@ interface ViewModelModule { @IntoMap @ViewModelKey(RoomDetailSharedActionViewModel::class) fun bindRoomDetailSharedActionViewModel(viewModel: RoomDetailSharedActionViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(RoomProfileSharedActionViewModel::class) + fun bindRoomProfileSharedActionViewModel(viewModel: RoomProfileSharedActionViewModel): ViewModel } diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSeparatorItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/DividerItem.kt similarity index 69% rename from vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSeparatorItem.kt rename to vector/src/main/java/im/vector/riotx/core/epoxy/DividerItem.kt index dd41d5dd66..ab849a6909 100644 --- a/vector/src/main/java/im/vector/riotx/core/epoxy/bottomsheet/BottomSheetSeparatorItem.kt +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/DividerItem.kt @@ -12,17 +12,14 @@ * 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.riotx.core.epoxy.bottomsheet +package im.vector.riotx.core.epoxy import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R -import im.vector.riotx.core.epoxy.VectorEpoxyHolder -import im.vector.riotx.core.epoxy.VectorEpoxyModel -@EpoxyModelClass(layout = R.layout.item_bottom_sheet_divider) -abstract class BottomSheetSeparatorItem : VectorEpoxyModel() { +@EpoxyModelClass(layout = R.layout.item_divider) +abstract class DividerItem : VectorEpoxyModel() { class Holder : VectorEpoxyHolder() } diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt new file mode 100644 index 0000000000..0a7dcb0de7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileActionItem.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 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.riotx.core.epoxy.profiles + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.themes.ThemeUtils + +@EpoxyModelClass(layout = R.layout.item_profile_action) +abstract class ProfileActionItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var title: String + @EpoxyAttribute + var subtitle: String? = null + @EpoxyAttribute + var iconRes: Int = 0 + @EpoxyAttribute + var editable: Boolean = true + @EpoxyAttribute + var destructive: Boolean = false + @EpoxyAttribute + lateinit var listener: View.OnClickListener + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.setOnClickListener(listener) + holder.editable.isVisible = editable + holder.title.text = title + val tintColor = if (destructive) { + ContextCompat.getColor(holder.view.context, R.color.riotx_notice) + } else { + ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_primary) + } + holder.title.setTextColor(tintColor) + holder.subtitle.setTextOrHide(subtitle) + if (iconRes != 0) { + holder.icon.setImageResource(iconRes) + holder.icon.isVisible = true + } else { + holder.icon.isVisible = false + } + } + + class Holder : VectorEpoxyHolder() { + val icon by bind(R.id.actionIcon) + val title by bind(R.id.actionTitle) + val subtitle by bind(R.id.actionSubtitle) + val editable by bind(R.id.actionEditable) + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt new file mode 100644 index 0000000000..3a9fff52af --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileItemExtensions.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.core.epoxy.profiles + +import androidx.annotation.DrawableRes +import com.airbnb.epoxy.EpoxyController +import im.vector.riotx.core.epoxy.DividerItem_ + +fun EpoxyController.buildProfileSection(title: String) { + profileSectionItem { + id("section_$title") + title(title) + } +} + +fun EpoxyController.buildProfileAction( + id: String, + title: String, + subtitle: String? = null, + editable: Boolean = true, + @DrawableRes icon: Int = 0, + destructive: Boolean = false, + divider: Boolean = true, + action: () -> Unit +) { + profileActionItem { + iconRes(icon) + id("action_$id") + subtitle(subtitle) + editable(editable) + destructive(destructive) + title(title) + listener { _ -> + action() + } + } + + DividerItem_() + .id("divider_$title") + .addIf(divider, this) +} diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt new file mode 100644 index 0000000000..4fe65748ce --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileMatrixItem.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.core.epoxy.profiles + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.home.AvatarRenderer + +@EpoxyModelClass(layout = R.layout.item_profile_matrix_item) +abstract class ProfileMatrixItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute lateinit var matrixItem: MatrixItem + @EpoxyAttribute var clickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + val bestName = matrixItem.getBestName() + val matrixId = matrixItem.id.takeIf { it != bestName } + holder.view.setOnClickListener(clickListener) + holder.titleView.text = bestName + holder.subtitleView.setTextOrHide(matrixId) + avatarRenderer.render(matrixItem, holder.avatarImageView) + } + + class Holder : VectorEpoxyHolder() { + val titleView by bind(R.id.matrixItemTitle) + val subtitleView by bind(R.id.matrixItemSubtitle) + val avatarImageView by bind(R.id.matrixItemAvatar) + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileSectionItem.kt b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileSectionItem.kt new file mode 100644 index 0000000000..ea3e66ad03 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/epoxy/profiles/ProfileSectionItem.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 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.riotx.core.epoxy.profiles + +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.epoxy.VectorEpoxyModel + +@EpoxyModelClass(layout = R.layout.item_profile_section) +abstract class ProfileSectionItem: VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var title: String + + override fun bind(holder: Holder) { + super.bind(holder) + holder.sectionView.text = title + } + + class Holder : VectorEpoxyHolder() { + val sectionView by bind(R.id.itemProfileSectionView) + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt b/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt index f674478724..4c5a987b4b 100755 --- a/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/StateView.kt @@ -34,7 +34,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? data class Error(val message: CharSequence? = null) : State() } - private var eventCallback: EventCallback? = null + var eventCallback: EventCallback? = null var contentView: View? = null diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index f4e3631b8a..5c73fc97da 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -23,11 +23,18 @@ import android.os.Parcelable import android.view.Menu import android.view.MenuItem import android.view.View -import androidx.annotation.* +import androidx.annotation.AttrRes +import androidx.annotation.LayoutRes +import androidx.annotation.MainThread +import androidx.annotation.MenuRes +import androidx.annotation.Nullable +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider @@ -41,7 +48,12 @@ import com.google.android.material.snackbar.Snackbar import im.vector.matrix.android.api.failure.GlobalError import im.vector.riotx.BuildConfig import im.vector.riotx.R -import im.vector.riotx.core.di.* +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.di.DaggerScreenComponent +import im.vector.riotx.core.di.HasScreenInjector +import im.vector.riotx.core.di.HasVectorInjector +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.di.VectorComponent import im.vector.riotx.core.dialogs.DialogLocker import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.utils.toast @@ -92,6 +104,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { lateinit var rageShake: RageShake private set protected lateinit var navigator: Navigator + private lateinit var fragmentFactory: FragmentFactory private lateinit var activeSessionHolder: ActiveSessionHolder private lateinit var vectorPreferences: VectorPreferences @@ -145,7 +158,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { } Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms") ThemeUtils.setActivityTheme(this, getOtherThemes()) - supportFragmentManager.fragmentFactory = screenComponent.fragmentFactory() + fragmentFactory = screenComponent.fragmentFactory() + supportFragmentManager.fragmentFactory = fragmentFactory super.onCreate(savedInstanceState) viewModelFactory = screenComponent.viewModelFactory() configurationViewModel = viewModelProvider.get(ConfigurationViewModel::class.java) @@ -196,7 +210,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { handleInvalidToken(globalError) is GlobalError.ConsentNotGivenError -> consentNotGivenHelper.displayDialog(globalError.consentUri, - activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "") + activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host + ?: "") } } @@ -209,11 +224,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { mainActivityStarted = true MainActivity.restartApp(this, - MainActivityArgs( - clearCredentials = !globalError.softLogout, - isUserLoggedOut = true, - isSoftLogout = globalError.softLogout - ) + MainActivityArgs( + clearCredentials = !globalError.softLogout, + isUserLoggedOut = true, + isSoftLogout = globalError.softLogout + ) ) } @@ -276,6 +291,12 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { protected open fun injectWith(injector: ScreenComponent) = Unit + protected fun createFragment(fragmentClass: Class, args: Bundle?): Fragment { + return fragmentFactory.instantiate(classLoader, fragmentClass.name).apply { + arguments = args + } + } + /* ========================================================================================== * PRIVATE METHODS * ========================================================================================== */ @@ -372,12 +393,10 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { */ protected fun configureToolbar(toolbar: Toolbar, displayBack: Boolean = true) { setSupportActionBar(toolbar) - - if (displayBack) { - supportActionBar?.let { - it.setDisplayShowHomeEnabled(true) - it.setDisplayHomeAsUpEnabled(true) - } + supportActionBar?.let { + it.setDisplayShowHomeEnabled(displayBack) + it.setDisplayHomeAsUpEnabled(displayBack) + it.title = null } } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index 3bb667593c..91c166a96c 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -14,8 +14,11 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION") + package im.vector.riotx.core.platform +import android.app.ProgressDialog import android.content.Context import android.os.Bundle import android.os.Parcelable @@ -59,6 +62,8 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { protected lateinit var navigator: Navigator protected lateinit var errorFormatter: ErrorFormatter + private var progress: ProgressDialog? = null + /* ========================================================================================== * View model * ========================================================================================== */ @@ -96,6 +101,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { } final override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + Timber.i("onCreateView Fragment ${this.javaClass.simpleName}") return inflater.inflate(getLayoutResId(), container, false) } @@ -117,6 +123,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { @CallSuper override fun onDestroyView() { super.onDestroyView() + Timber.i("onDestroyView Fragment ${this.javaClass.simpleName}") mUnBinder?.unbind() mUnBinder = null uiDisposables.clear() @@ -175,6 +182,19 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { } } + protected fun showLoadingDialog(message: CharSequence, cancelable: Boolean = false) { + progress = ProgressDialog(requireContext()).apply { + setCancelable(cancelable) + setMessage(message) + setProgressStyle(ProgressDialog.STYLE_SPINNER) + show() + } + } + + protected fun dismissLoadingDialog() { + progress?.dismiss() + } + /* ========================================================================================== * Toolbar * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt b/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt index 726d2ea697..4c4a553e5c 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/DataSource.kt @@ -19,7 +19,7 @@ package im.vector.riotx.core.utils import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.PublishRelay import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers +import io.reactivex.android.schedulers.AndroidSchedulers interface DataSource { fun observe(): Observable @@ -37,7 +37,7 @@ open class BehaviorDataSource(private val defaultValue: T? = null) : MutableD private val behaviorRelay = createRelay() override fun observe(): Observable { - return behaviorRelay.hide().observeOn(Schedulers.computation()) + return behaviorRelay.hide().observeOn(AndroidSchedulers.mainThread()) } override fun post(value: T) { @@ -61,7 +61,7 @@ open class PublishDataSource : MutableDataSource { private val publishRelay = PublishRelay.create() override fun observe(): Observable { - return publishRelay.hide() + return publishRelay.hide().observeOn(AndroidSchedulers.mainThread()) } override fun post(value: T) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/UserColor.kt b/vector/src/main/java/im/vector/riotx/core/utils/UserColor.kt similarity index 96% rename from vector/src/main/java/im/vector/riotx/features/home/UserColor.kt rename to vector/src/main/java/im/vector/riotx/core/utils/UserColor.kt index d34ca6506a..1f8308cd5c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/UserColor.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/UserColor.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home +package im.vector.riotx.core.utils import androidx.annotation.ColorRes import im.vector.riotx.R diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt index 1c8dc99196..8102a5a45f 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberController.kt @@ -17,20 +17,20 @@ package im.vector.riotx.features.autocomplete.member import com.airbnb.epoxy.TypedEpoxyController -import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.autocomplete.autocompleteMatrixItem import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject -class AutocompleteMemberController @Inject constructor() : TypedEpoxyController>() { +class AutocompleteMemberController @Inject constructor() : TypedEpoxyController>() { - var listener: AutocompleteClickListener? = null + var listener: AutocompleteClickListener? = null @Inject lateinit var avatarRenderer: AvatarRenderer - override fun buildModels(data: List?) { + override fun buildModels(data: List?) { if (data.isNullOrEmpty()) { return } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt index bc50b12e6e..4be19f2e73 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/member/AutocompleteMemberPresenter.kt @@ -25,14 +25,14 @@ import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.riotx.features.autocomplete.AutocompleteClickListener class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, @Assisted val roomId: String, private val session: Session, private val controller: AutocompleteMemberController -) : RecyclerViewPresenter(context), AutocompleteClickListener { +) : RecyclerViewPresenter(context), AutocompleteClickListener { private val room = session.getRoom(roomId)!! @@ -51,7 +51,7 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, return controller.adapter } - override fun onItemClick(t: RoomMember) { + override fun onItemClick(t: RoomMemberSummary) { dispatchClick(t) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomAction.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt similarity index 95% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomAction.kt rename to vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt index 8410a95707..0e74ff71fd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomAction.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.createdirect +package im.vector.riotx.features.createdirect import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.core.platform.VectorViewModelAction diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt similarity index 98% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt rename to vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt index f7a68d4552..2ca97a0f18 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt @@ -16,7 +16,7 @@ * */ -package im.vector.riotx.features.home.createdirect +package im.vector.riotx.features.createdirect import android.content.Context import android.content.Intent diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomDirectoryUsersFragment.kt similarity index 98% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt rename to vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomDirectoryUsersFragment.kt index 4230ea030d..77cee5fd6f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomDirectoryUsersFragment.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.createdirect +package im.vector.riotx.features.createdirect import android.os.Bundle import android.view.View diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt similarity index 99% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt rename to vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt index 8108e9705c..c3a66dce1e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomKnownUsersFragment.kt @@ -16,7 +16,7 @@ * */ -package im.vector.riotx.features.home.createdirect +package im.vector.riotx.features.createdirect import android.os.Bundle import android.view.Menu diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomLetterHeaderItem.kt similarity index 96% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt rename to vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomLetterHeaderItem.kt index d87abc852c..e512337c64 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomLetterHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomLetterHeaderItem.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.createdirect +package im.vector.riotx.features.createdirect import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedAction.kt similarity index 94% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomSharedAction.kt rename to vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedAction.kt index 0df6720734..eeffc1f119 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomSharedAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedAction.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.createdirect +package im.vector.riotx.features.createdirect import im.vector.riotx.core.platform.VectorSharedAction diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedActionViewModel.kt similarity index 94% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomSharedActionViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedActionViewModel.kt index 590a4855f5..91c21378d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomSharedActionViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomSharedActionViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.createdirect +package im.vector.riotx.features.createdirect import im.vector.riotx.core.platform.VectorSharedActionViewModel import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomUserItem.kt similarity index 98% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt rename to vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomUserItem.kt index 401d4445fe..f2f517fd6e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomUserItem.kt @@ -16,7 +16,7 @@ * */ -package im.vector.riotx.features.home.createdirect +package im.vector.riotx.features.createdirect import android.view.View import android.widget.ImageView diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt similarity index 99% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt index b71b0cd9d0..9023312530 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewModel.kt @@ -16,7 +16,7 @@ * */ -package im.vector.riotx.features.home.createdirect +package im.vector.riotx.features.createdirect import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt similarity index 96% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt rename to vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt index 15062fb961..dcf86ef6f1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomViewState.kt @@ -16,7 +16,7 @@ * */ -package im.vector.riotx.features.home.createdirect +package im.vector.riotx.features.createdirect import androidx.paging.PagedList import arrow.core.Option diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt similarity index 98% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt rename to vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt index 8d2b3928be..016806f319 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt @@ -16,7 +16,7 @@ * */ -package im.vector.riotx.features.home.createdirect +package im.vector.riotx.features.createdirect import com.airbnb.epoxy.EpoxyController import com.airbnb.mvrx.Fail diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/KnownUsersController.kt similarity index 98% rename from vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt rename to vector/src/main/java/im/vector/riotx/features/createdirect/KnownUsersController.kt index 8270683975..a0e20b45f5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/KnownUsersController.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.createdirect +package im.vector.riotx.features.createdirect import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.paging.PagedListEpoxyController diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListAction.kt b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListAction.kt similarity index 94% rename from vector/src/main/java/im/vector/riotx/features/home/group/GroupListAction.kt rename to vector/src/main/java/im/vector/riotx/features/grouplist/GroupListAction.kt index e81890e7f2..3697989af7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListAction.kt @@ -12,9 +12,10 @@ * 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.riotx.features.home.group +package im.vector.riotx.features.grouplist import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotx.core.platform.VectorViewModelAction diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListFragment.kt similarity index 98% rename from vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt rename to vector/src/main/java/im/vector/riotx/features/grouplist/GroupListFragment.kt index 254571f8cf..c9c98a58ab 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListFragment.kt @@ -12,9 +12,10 @@ * 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.riotx.features.home.group +package im.vector.riotx.features.grouplist import android.os.Bundle import android.view.View diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt similarity index 99% rename from vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt rename to vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt index a00ee24b49..816f721040 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewModel.kt @@ -12,9 +12,10 @@ * 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.riotx.features.home.group +package im.vector.riotx.features.grouplist import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewState.kt b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewState.kt similarity index 95% rename from vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewState.kt rename to vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewState.kt index 7b724d7e8d..ef8fd6ea6f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupListViewState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.group +package im.vector.riotx.features.grouplist import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupSummaryController.kt similarity index 97% rename from vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt rename to vector/src/main/java/im/vector/riotx/features/grouplist/GroupSummaryController.kt index 95054d1689..bc8e87b023 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt +++ b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupSummaryController.kt @@ -12,9 +12,10 @@ * 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.riotx.features.home.group +package im.vector.riotx.features.grouplist import com.airbnb.epoxy.EpoxyController import im.vector.matrix.android.api.session.group.model.GroupSummary diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryItem.kt b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupSummaryItem.kt similarity index 97% rename from vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryItem.kt rename to vector/src/main/java/im/vector/riotx/features/grouplist/GroupSummaryItem.kt index 61c589cc00..129e562581 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/grouplist/GroupSummaryItem.kt @@ -12,9 +12,10 @@ * 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.riotx.features.home.group +package im.vector.riotx.features.grouplist import android.widget.ImageView import android.widget.TextView diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/SelectedGroupDataSource.kt b/vector/src/main/java/im/vector/riotx/features/grouplist/SelectedGroupDataSource.kt similarity index 95% rename from vector/src/main/java/im/vector/riotx/features/home/group/SelectedGroupDataSource.kt rename to vector/src/main/java/im/vector/riotx/features/grouplist/SelectedGroupDataSource.kt index c7b36e1e7f..a8f6b62cab 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/SelectedGroupDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/grouplist/SelectedGroupDataSource.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.riotx.features.home.group +package im.vector.riotx.features.grouplist import arrow.core.Option import im.vector.matrix.android.api.session.group.model.GroupSummary diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt index c3d16f3299..ac4d29dd96 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt @@ -32,6 +32,7 @@ import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideRequest import im.vector.riotx.core.glide.GlideRequests +import im.vector.riotx.core.utils.getColorFromUserId import javax.inject.Inject /** diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt index ca552dc234..d6d8f06e60 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewModel.kt @@ -26,7 +26,7 @@ import im.vector.matrix.rx.rx import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.features.home.group.SelectedGroupDataSource +import im.vector.riotx.features.grouplist.SelectedGroupDataSource import im.vector.riotx.features.ui.UiStateRepository import io.reactivex.schedulers.Schedulers diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt index bc3bc2f9d5..9aa9313ad2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt @@ -24,7 +24,7 @@ import im.vector.riotx.R import im.vector.riotx.core.extensions.observeK import im.vector.riotx.core.extensions.replaceChildFragment import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.features.home.group.GroupListFragment +import im.vector.riotx.features.grouplist.GroupListFragment import kotlinx.android.synthetic.main.fragment_home_drawer.* import javax.inject.Inject diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 7ca647ea3e..314baab011 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -27,7 +27,7 @@ import com.otaliastudios.autocomplete.CharPolicy import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.group.model.GroupSummary -import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem @@ -108,13 +108,13 @@ class AutoCompleter @AssistedInject constructor( private fun setupMembers(backgroundDrawable: ColorDrawable, editText: EditText) { val autocompleteMemberPresenter = autocompleteMemberPresenterFactory.create(roomId) - Autocomplete.on(editText) + Autocomplete.on(editText) .with(CharPolicy('@', true)) .with(autocompleteMemberPresenter) .with(ELEVATION) .with(backgroundDrawable) - .with(object : AutocompleteCallback { - override fun onPopupItemClicked(editable: Editable, item: RoomMember): Boolean { + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: RoomMemberSummary): Boolean { insertMatrixItem(editText, editable, "@", item.toMatrixItem()) return true } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 675b2242b8..0cae97b562 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -26,7 +26,12 @@ import android.os.Build import android.os.Bundle import android.os.Parcelable import android.text.Spannable -import android.view.* +import android.view.HapticFeedbackConstants +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.Window import android.widget.TextView import android.widget.Toast import androidx.annotation.DrawableRes @@ -46,7 +51,13 @@ import butterknife.BindView import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.OnModelBuildFinishedListener -import com.airbnb.mvrx.* +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader import com.google.android.material.snackbar.Snackbar @@ -57,7 +68,13 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent +import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent +import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -67,19 +84,36 @@ import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.R import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer -import im.vector.riotx.core.extensions.* +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.hideKeyboard +import im.vector.riotx.core.extensions.observeEvent +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.extensions.showKeyboard import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.ui.views.JumpToReadMarkerView import im.vector.riotx.core.ui.views.NotificationAreaView -import im.vector.riotx.core.utils.* +import im.vector.riotx.core.utils.Debouncer +import im.vector.riotx.core.utils.KeyboardStateUtils +import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_INCOMING_URI +import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_PICK_ATTACHMENT +import im.vector.riotx.core.utils.TextUtils +import im.vector.riotx.core.utils.allGranted +import im.vector.riotx.core.utils.checkPermissions +import im.vector.riotx.core.utils.copyToClipboard +import im.vector.riotx.core.utils.createUIHandler +import im.vector.riotx.core.utils.getColorFromUserId +import im.vector.riotx.core.utils.openUrlInExternalBrowser +import im.vector.riotx.core.utils.shareMedia +import im.vector.riotx.core.utils.toast import im.vector.riotx.features.attachments.AttachmentTypeSelectorView import im.vector.riotx.features.attachments.AttachmentsHelper import im.vector.riotx.features.attachments.ContactAttachment import im.vector.riotx.features.command.Command import im.vector.riotx.features.home.AvatarRenderer -import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.room.detail.composer.TextComposerView import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -87,7 +121,12 @@ import im.vector.riotx.features.home.room.detail.timeline.action.EventSharedActi import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet -import im.vector.riotx.features.home.room.detail.timeline.item.* +import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem +import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem +import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.PillImageSpan @@ -97,7 +136,7 @@ import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.notifications.NotificationDrawerManager -import im.vector.riotx.features.permalink.NavigateToRoomInterceptor +import im.vector.riotx.features.permalink.NavigationInterceptor import im.vector.riotx.features.permalink.PermalinkHandler import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.settings.VectorPreferences @@ -205,7 +244,9 @@ class RoomDetailFragment @Inject constructor( setupNotificationView() setupJumpToReadMarkerView() setupJumpToBottomView() - + roomToolbarContentView.setOnClickListener { + navigator.openRoomProfile(requireActivity(), roomDetailArgs.roomId) + } roomDetailViewModel.subscribe { renderState(it) } roomDetailViewModel.sendMessageResultLiveData.observeEvent(viewLifecycleOwner) { renderSendMessageResult(it) } @@ -328,9 +369,9 @@ class RoomDetailFragment @Inject constructor( AlertDialog.Builder(requireActivity()) .setTitle(R.string.dialog_title_error) .setMessage(getString(R.string.error_file_too_big, - error.filename, - TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), - TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) + error.filename, + TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes), + TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes) )) .setPositiveButton(R.string.ok, null) .show() @@ -417,9 +458,10 @@ class RoomDetailFragment @Inject constructor( avatarRenderer.render( MatrixItem.UserItem(event.root.senderId - ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), + ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar), composerLayout.composerRelatedMessageAvatar ) + composerLayout.expand { if (isAdded) { // need to do it here also when not using quick reply @@ -435,7 +477,7 @@ class RoomDetailFragment @Inject constructor( // Ignore update to avoid saving a draft composerLayout.composerEditText.setText(text) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length - ?: 0) + ?: 0) } } @@ -799,7 +841,7 @@ class RoomDetailFragment @Inject constructor( override fun onUrlClicked(url: String): Boolean { permalinkHandler - .launch(requireActivity(), url, object : NavigateToRoomInterceptor { + .launch(requireActivity(), url, object : NavigationInterceptor { override fun navToRoom(roomId: String?, eventId: String?): Boolean { // Same room? if (roomId == roomDetailArgs.roomId) { @@ -815,6 +857,11 @@ class RoomDetailFragment @Inject constructor( // Not handled return false } + + override fun navToMemberProfile(userId: String): Boolean { + navigator.openRoomMemberProfile(userId, roomDetailArgs.roomId, vectorBaseActivity) + return true + } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -944,7 +991,7 @@ class RoomDetailFragment @Inject constructor( } override fun onAvatarClicked(informationData: MessageInformationData) { - vectorBaseActivity.notImplemented("Click on user avatar") + navigator.openRoomMemberProfile(userId = informationData.senderId, roomId = roomDetailArgs.roomId, context = requireActivity()) } override fun onMemberNameClicked(informationData: MessageInformationData) { @@ -973,7 +1020,7 @@ class RoomDetailFragment @Inject constructor( override fun onRoomCreateLinkClicked(url: String) { permalinkHandler - .launch(requireContext(), url, object : NavigateToRoomInterceptor { + .launch(requireContext(), url, object : NavigationInterceptor { override fun navToRoom(roomId: String?, eventId: String?): Boolean { requireActivity().finish() return false @@ -1124,7 +1171,7 @@ class RoomDetailFragment @Inject constructor( val startToCompose = composerLayout.composerEditText.text.isNullOrBlank() if (startToCompose - && userId == session.myUserId) { + && userId == session.myUserId) { // Empty composer, current user: start an emote composerLayout.composerEditText.setText(Command.EMOTE.command + " ") composerLayout.composerEditText.setSelection(Command.EMOTE.length) @@ -1162,7 +1209,6 @@ class RoomDetailFragment @Inject constructor( } } } - focusComposerAndShowKeyboard() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 94291905e9..41c90504f5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -35,7 +35,7 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.file.FileService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType @@ -207,7 +207,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro invisibleEventsObservable.accept(action) } - fun getMember(userId: String): RoomMember? { + fun getMember(userId: String): RoomMemberSummary? { return room.getRoomMember(userId) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 9a2fb4b6de..ac77e5de3d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -21,6 +21,7 @@ import com.airbnb.mvrx.Success import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.R import im.vector.riotx.core.epoxy.bottomsheet.* +import im.vector.riotx.core.epoxy.dividerItem import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController @@ -70,7 +71,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid // Quick reactions if (state.canReact() && state.quickStates is Success) { // Separator - bottomSheetSeparatorItem { + dividerItem { id("reaction_separator") } @@ -88,14 +89,14 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid } // Separator - bottomSheetSeparatorItem { + dividerItem { id("actions_separator") } // Action state.actions.forEachIndexed { index, action -> if (action is EventSharedAction.Separator) { - bottomSheetSeparatorItem { + dividerItem { id("separator_$index") } } else { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 7ee8486ba2..0a8f1e11bb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -24,7 +24,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.ColorProvider -import im.vector.riotx.features.home.getColorFromUserId +import im.vector.riotx.core.utils.getColorFromUserId import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index 122b95aa52..4d9f5fb847 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -39,6 +39,7 @@ import im.vector.riotx.core.platform.OnBackPressed import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.features.home.RoomListDisplayMode +import im.vector.riotx.features.home.room.list.actions.RoomListActionsArgs import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel @@ -228,7 +229,7 @@ class RoomListFragment @Inject constructor( roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.MUTE)) } is RoomListQuickActionsSharedAction.Settings -> { - vectorBaseActivity.notImplemented("Opening room settings") + navigator.openRoomProfile(requireActivity(), quickAction.roomId) } is RoomListQuickActionsSharedAction.Leave -> { AlertDialog.Builder(requireContext()) @@ -346,7 +347,7 @@ class RoomListFragment @Inject constructor( roomController.onRoomLongClicked() RoomListQuickActionsBottomSheet - .newInstance(room.roomId) + .newInstance(room.roomId, RoomListActionsArgs.Mode.FULL) .show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS") return true } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt index 5fc33ffbe9..1e775a934a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsBottomSheet.kt @@ -37,8 +37,15 @@ import javax.inject.Inject @Parcelize data class RoomListActionsArgs( - val roomId: String -) : Parcelable + val roomId: String, + val mode: Mode +) : Parcelable { + + enum class Mode { + FULL, + NOTIFICATIONS + } +} /** * Bottom sheet fragment that shows room information with list of contextual actions @@ -93,9 +100,9 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R } companion object { - fun newInstance(roomId: String): RoomListQuickActionsBottomSheet { + fun newInstance(roomId: String, mode: RoomListActionsArgs.Mode): RoomListQuickActionsBottomSheet { return RoomListQuickActionsBottomSheet().apply { - setArguments(RoomListActionsArgs(roomId)) + setArguments(RoomListActionsArgs(roomId, mode)) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt index 8d25f5713a..0948490d12 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.room.notification.RoomNotificationSt import im.vector.matrix.android.api.util.toMatrixItem import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetActionItem import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetRoomPreviewItem -import im.vector.riotx.core.epoxy.bottomsheet.bottomSheetSeparatorItem +import im.vector.riotx.core.epoxy.dividerItem import im.vector.riotx.features.home.AvatarRenderer import javax.inject.Inject @@ -35,18 +35,21 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar override fun buildModels(state: RoomListQuickActionsState) { val roomSummary = state.roomSummary() ?: return + val showAll = state.mode == RoomListActionsArgs.Mode.FULL - // Preview - bottomSheetRoomPreviewItem { - id("preview") - avatarRenderer(avatarRenderer) - matrixItem(roomSummary.toMatrixItem()) - settingsClickListener(View.OnClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.Settings(roomSummary.roomId)) }) - } + if (showAll) { + // Preview + bottomSheetRoomPreviewItem { + id("room_preview") + avatarRenderer(avatarRenderer) + matrixItem(roomSummary.toMatrixItem()) + settingsClickListener(View.OnClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.Settings(roomSummary.roomId)) }) + } - // Notifications - bottomSheetSeparatorItem { - id("notifications_separator") + // Notifications + dividerItem { + id("notifications_separator") + } } val selectedRoomState = state.roomNotificationState() @@ -55,11 +58,13 @@ class RoomListQuickActionsEpoxyController @Inject constructor(private val avatar RoomListQuickActionsSharedAction.NotificationsMentionsOnly(roomSummary.roomId).toBottomSheetItem(2, selectedRoomState) RoomListQuickActionsSharedAction.NotificationsMute(roomSummary.roomId).toBottomSheetItem(3, selectedRoomState) - // Leave - bottomSheetSeparatorItem { - id("leave_separator") + if (showAll) { + // Leave + dividerItem { + id("leave_separator") + } + RoomListQuickActionsSharedAction.Leave(roomSummary.roomId).toBottomSheetItem(5) } - RoomListQuickActionsSharedAction.Leave(roomSummary.roomId).toBottomSheetItem(5) } private fun RoomListQuickActionsSharedAction.toBottomSheetItem(index: Int, roomNotificationState: RoomNotificationState? = null) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsState.kt index a943db1804..6e134871a7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/actions/RoomListQuickActionsState.kt @@ -24,9 +24,10 @@ import im.vector.matrix.android.api.session.room.notification.RoomNotificationSt data class RoomListQuickActionsState( val roomId: String, + val mode: RoomListActionsArgs.Mode, val roomSummary: Async = Uninitialized, val roomNotificationState: Async = Uninitialized ) : MvRxState { - constructor(args: RoomListActionsArgs) : this(roomId = args.roomId) + constructor(args: RoomListActionsArgs) : this(roomId = args.roomId, mode = args.mode) } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 8784ec662d..7ca6e94d70 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -26,20 +26,22 @@ import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.error.fatalError import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.utils.toast +import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.riotx.features.debug.DebugMenuActivity -import im.vector.riotx.features.home.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.home.room.detail.RoomDetailActivity import im.vector.riotx.features.home.room.detail.RoomDetailArgs import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity +import im.vector.riotx.features.roommemberprofile.RoomMemberProfileActivity +import im.vector.riotx.features.roommemberprofile.RoomMemberProfileArgs +import im.vector.riotx.features.roomprofile.RoomProfileActivity import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.share.SharedData -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -54,16 +56,9 @@ class DefaultNavigator @Inject constructor( fatalError("Trying to open an unknown room $roomId", vectorPreferences.failFast()) return } - val args = RoomDetailArgs(roomId, eventId) val intent = RoomDetailActivity.newIntent(context, args) - if (buildTask) { - val stackBuilder = TaskStackBuilder.create(context) - stackBuilder.addNextIntentWithParentStack(intent) - stackBuilder.startActivities() - } else { - context.startActivity(intent) - } + startActivity(context, intent, buildTask) } override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String?, buildTask: Boolean) { @@ -82,12 +77,10 @@ class DefaultNavigator @Inject constructor( } } - override fun openUserDetail(userId: String, context: Context, buildTask: Boolean) { - if (context is VectorBaseActivity) { - context.notImplemented("Open user detail") - } else { - context.toast(R.string.not_implemented) - } + override fun openRoomMemberProfile(userId: String, roomId: String?, context: Context, buildTask: Boolean) { + val args = RoomMemberProfileArgs(userId = userId, roomId = roomId) + val intent = RoomMemberProfileActivity.newIntent(context, args) + startActivity(context, intent, buildTask) } override fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData) { @@ -139,7 +132,17 @@ class DefaultNavigator @Inject constructor( context.startActivity(KeysBackupManageActivity.intent(context)) } - override fun openRoomSettings(context: Context, roomId: String) { - Timber.v("Open room settings$roomId") + override fun openRoomProfile(context: Context, roomId: String) { + context.startActivity(RoomProfileActivity.newIntent(context, roomId)) + } + + private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) { + if (buildTask) { + val stackBuilder = TaskStackBuilder.create(context) + stackBuilder.addNextIntentWithParentStack(intent) + stackBuilder.startActivities() + } else { + context.startActivity(intent) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 60045984c3..e2bca2453f 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -50,7 +50,7 @@ interface Navigator { fun openGroupDetail(groupId: String, context: Context, buildTask: Boolean = false) - fun openUserDetail(userId: String, context: Context, buildTask: Boolean = false) + fun openRoomMemberProfile(userId: String, roomId: String?, context: Context, buildTask: Boolean = false) - fun openRoomSettings(context: Context, roomId: String) + fun openRoomProfile(context: Context, roomId: String) } diff --git a/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt index e46adc53fc..84a25060cc 100644 --- a/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/permalink/PermalinkHandler.kt @@ -35,17 +35,17 @@ class PermalinkHandler @Inject constructor(private val session: Session, fun launch( context: Context, deepLink: String?, - navigateToRoomInterceptor: NavigateToRoomInterceptor? = null, + navigationInterceptor: NavigationInterceptor? = null, buildTask: Boolean = false ): Single { val uri = deepLink?.let { Uri.parse(it) } - return launch(context, uri, navigateToRoomInterceptor, buildTask) + return launch(context, uri, navigationInterceptor, buildTask) } fun launch( context: Context, deepLink: Uri?, - navigateToRoomInterceptor: NavigateToRoomInterceptor? = null, + navigationInterceptor: NavigationInterceptor? = null, buildTask: Boolean = false ): Single { if (deepLink == null) { @@ -57,7 +57,7 @@ class PermalinkHandler @Inject constructor(private val session: Session, .observeOn(AndroidSchedulers.mainThread()) .map { val roomId = it.getOrNull() - if (navigateToRoomInterceptor?.navToRoom(roomId, permalinkData.eventId) != true) { + if (navigationInterceptor?.navToRoom(roomId, permalinkData.eventId) != true) { openRoom(context, roomId, permalinkData.eventId, buildTask) } true @@ -68,7 +68,9 @@ class PermalinkHandler @Inject constructor(private val session: Session, Single.just(true) } is PermalinkData.UserLink -> { - navigator.openUserDetail(permalinkData.userId, context, buildTask) + if (navigationInterceptor?.navToMemberProfile(permalinkData.userId) != true) { + navigator.openRoomMemberProfile(userId = permalinkData.userId, roomId = null, context = context, buildTask = buildTask) + } Single.just(true) } is PermalinkData.FallbackLink -> { @@ -98,10 +100,19 @@ class PermalinkHandler @Inject constructor(private val session: Session, } } -interface NavigateToRoomInterceptor { +interface NavigationInterceptor { /** * Return true if the navigation has been intercepted */ - fun navToRoom(roomId: String?, eventId: String? = null): Boolean + fun navToRoom(roomId: String?, eventId: String? = null): Boolean { + return false + } + + /** + * Return true if the navigation has been intercepted + */ + fun navToMemberProfile(userId: String): Boolean { + return false + } } diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt new file mode 100644 index 0000000000..8ff209b443 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileAction.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.features.roommemberprofile + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class RoomMemberProfileAction : VectorViewModelAction { + + object RetryFetchingInfo: RoomMemberProfileAction() + object IgnoreUser: RoomMemberProfileAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileActivity.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileActivity.kt new file mode 100644 index 0000000000..25efecf541 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileActivity.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.features.roommemberprofile + +import android.content.Context +import android.content.Intent +import androidx.appcompat.widget.Toolbar +import im.vector.riotx.R +import im.vector.riotx.core.extensions.addFragment +import im.vector.riotx.core.platform.ToolbarConfigurable +import im.vector.riotx.core.platform.VectorBaseActivity + +class RoomMemberProfileActivity : VectorBaseActivity(), ToolbarConfigurable { + + companion object { + + private const val EXTRA_FRAGMENT_ARGS = "EXTRA_FRAGMENT_ARGS" + + fun newIntent(context: Context, args: RoomMemberProfileArgs): Intent { + return Intent(context, RoomMemberProfileActivity::class.java).apply { + putExtra(EXTRA_FRAGMENT_ARGS, args) + } + } + } + + override fun getLayoutRes() = R.layout.activity_simple + + override fun initUiAndData() { + if (isFirstCreation()) { + val fragmentArgs: RoomMemberProfileArgs = intent?.extras?.getParcelable(EXTRA_FRAGMENT_ARGS) + ?: return + addFragment(R.id.simpleFragmentContainer, RoomMemberProfileFragment::class.java, fragmentArgs) + } + } + + override fun configure(toolbar: Toolbar) { + configureToolbar(toolbar) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt new file mode 100644 index 0000000000..a77b34b7ab --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileController.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.features.roommemberprofile + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.profiles.buildProfileAction +import im.vector.riotx.core.epoxy.profiles.buildProfileSection +import im.vector.riotx.core.resources.StringProvider +import javax.inject.Inject + +class RoomMemberProfileController @Inject constructor(private val stringProvider: StringProvider) + : TypedEpoxyController() { + + var callback: Callback? = null + + interface Callback { + fun onIgnoreClicked() + fun onLearnMoreClicked() + fun onJumpToReadReceiptClicked() + fun onMentionClicked() + } + + override fun buildModels(data: RoomMemberProfileViewState?) { + if (data?.userMatrixItem?.invoke() == null) { + return + } + if (data.showAsMember) { + buildRoomMemberActions(data) + } else { + buildUserActions(data) + } + } + + private fun buildUserActions(state: RoomMemberProfileViewState) { + val ignoreActionTitle = state.buildIgnoreActionTitle() ?: return + // More + buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) + buildProfileAction( + id = "ignore", + title = ignoreActionTitle, + destructive = true, + editable = false, + divider = false, + action = { callback?.onIgnoreClicked() } + ) + } + + private fun buildRoomMemberActions(state: RoomMemberProfileViewState) { + // Security + buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) + val learnMoreSubtitle = if (state.isRoomEncrypted) { + R.string.room_profile_encrypted_subtitle + } else { + R.string.room_profile_not_encrypted_subtitle + } + buildProfileAction( + id = "learn_more", + title = stringProvider.getString(R.string.room_profile_section_security_learn_more), + editable = false, + subtitle = stringProvider.getString(learnMoreSubtitle), + action = { callback?.onLearnMoreClicked() } + ) + + // More + if (!state.isMine) { + buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) + buildProfileAction( + id = "read_receipt", + editable = false, + title = stringProvider.getString(R.string.room_member_jump_to_read_receipt), + action = { callback?.onJumpToReadReceiptClicked() } + ) + + val ignoreActionTitle = state.buildIgnoreActionTitle() + + buildProfileAction( + id = "mention", + title = stringProvider.getString(R.string.room_participants_action_mention), + editable = false, + divider = ignoreActionTitle != null, + action = { callback?.onMentionClicked() } + ) + if (ignoreActionTitle != null) { + buildProfileAction( + id = "ignore", + title = ignoreActionTitle, + destructive = true, + editable = false, + divider = false, + action = { callback?.onIgnoreClicked() } + ) + } + } + } + + private fun RoomMemberProfileViewState.buildIgnoreActionTitle(): String? { + val isIgnored = isIgnored() ?: return null + return if (isIgnored) { + stringProvider.getString(R.string.unignore) + } else { + stringProvider.getString(R.string.ignore) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt new file mode 100644 index 0000000000..23db6b53a8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.features.roommemberprofile + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import com.airbnb.mvrx.* +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.riotx.R +import im.vector.riotx.core.animations.AppBarStateChangeListener +import im.vector.riotx.core.animations.MatrixItemAppBarStateChangeListener +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.platform.StateView +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.home.AvatarRenderer +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_matrix_profile.* +import kotlinx.android.synthetic.main.view_stub_room_member_profile_header.* +import javax.inject.Inject + +@Parcelize +data class RoomMemberProfileArgs( + val userId: String, + val roomId: String? = null +) : Parcelable + +class RoomMemberProfileFragment @Inject constructor( + val viewModelFactory: RoomMemberProfileViewModel.Factory, + private val roomMemberProfileController: RoomMemberProfileController, + private val avatarRenderer: AvatarRenderer +) : VectorBaseFragment(), RoomMemberProfileController.Callback { + + private val fragmentArgs: RoomMemberProfileArgs by args() + private val viewModel: RoomMemberProfileViewModel by fragmentViewModel() + + private lateinit var appBarStateChangeListener: AppBarStateChangeListener + + override fun getLayoutResId() = R.layout.fragment_matrix_profile + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupToolbar(matrixProfileToolbar) + val headerView = matrixProfileHeaderView.let { + it.layoutResource = R.layout.view_stub_room_member_profile_header + it.inflate() + } + memberProfileStateView.eventCallback = object : StateView.EventCallback { + override fun onRetryClicked() { + viewModel.handle(RoomMemberProfileAction.RetryFetchingInfo) + } + } + memberProfileStateView.contentView = memberProfileInfoContainer + matrixProfileRecyclerView.configureWith(roomMemberProfileController, hasFixedSize = true) + roomMemberProfileController.callback = this + appBarStateChangeListener = MatrixItemAppBarStateChangeListener(headerView, listOf(matrixProfileToolbarAvatarImageView, + matrixProfileToolbarTitleView)) + matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener) + viewModel.viewEvents + .observe() + .subscribe { + dismissLoadingDialog() + when (it) { + is RoomMemberProfileViewEvents.Loading -> showLoadingDialog(it.message) + is RoomMemberProfileViewEvents.Failure -> showErrorInSnackbar(it.throwable) + } + } + .disposeOnDestroyView() + } + + override fun onDestroyView() { + matrixProfileAppBarLayout.removeOnOffsetChangedListener(appBarStateChangeListener) + roomMemberProfileController.callback = null + matrixProfileRecyclerView.cleanup() + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { state -> + when (val asyncUserMatrixItem = state.userMatrixItem) { + is Incomplete -> { + matrixProfileToolbarTitleView.text = state.userId + avatarRenderer.render(MatrixItem.UserItem(state.userId, null, null), matrixProfileToolbarAvatarImageView) + memberProfileStateView.state = StateView.State.Loading + } + is Fail -> { + avatarRenderer.render(MatrixItem.UserItem(state.userId, null, null), matrixProfileToolbarAvatarImageView) + matrixProfileToolbarTitleView.text = state.userId + val failureMessage = errorFormatter.toHumanReadable(asyncUserMatrixItem.error) + memberProfileStateView.state = StateView.State.Error(failureMessage) + } + is Success -> { + val userMatrixItem = asyncUserMatrixItem() + memberProfileStateView.state = StateView.State.Content + memberProfileIdView.text = userMatrixItem.id + val bestName = userMatrixItem.getBestName() + memberProfileNameView.text = bestName + matrixProfileToolbarTitleView.text = bestName + avatarRenderer.render(userMatrixItem, memberProfileAvatarView) + avatarRenderer.render(userMatrixItem, matrixProfileToolbarAvatarImageView) + } + } + memberProfilePowerLevelView.setTextOrHide(state.userPowerLevelString()) + roomMemberProfileController.setData(state) + } + + // RoomMemberProfileController.Callback + + override fun onIgnoreClicked() { + viewModel.handle(RoomMemberProfileAction.IgnoreUser) + } + + override fun onLearnMoreClicked() { + vectorBaseActivity.notImplemented("Learn more") + } + + override fun onJumpToReadReceiptClicked() { + vectorBaseActivity.notImplemented("Jump to read receipts") + } + + override fun onMentionClicked() { + vectorBaseActivity.notImplemented("Mention") + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt new file mode 100644 index 0000000000..093e54989b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewEvents.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.roommemberprofile + +/** + * Transient events for RoomMemberProfile + */ +sealed class RoomMemberProfileViewEvents { + data class Loading(val message: CharSequence) : RoomMemberProfileViewEvents() + object OnIgnoreActionSuccess : RoomMemberProfileViewEvents() + data class Failure(val throwable: Throwable) : RoomMemberProfileViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt new file mode 100644 index 0000000000..88f394e865 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -0,0 +1,202 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.features.roommemberprofile + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.profile.ProfileService +import im.vector.matrix.android.api.session.room.Room +import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.rx.mapOptional +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.unwrap +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.utils.DataSource +import im.vector.riotx.core.utils.PublishDataSource +import io.reactivex.Observable +import io.reactivex.functions.BiFunction +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private val initialState: RoomMemberProfileViewState, + private val stringProvider: StringProvider, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RoomMemberProfileViewState): RoomMemberProfileViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomMemberProfileViewState): RoomMemberProfileViewModel? { + val fragment: RoomMemberProfileFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } + + private val _viewEvents = PublishDataSource() + val viewEvents: DataSource = _viewEvents + + private val room = if (initialState.roomId != null) { + session.getRoom(initialState.roomId) + } else { + null + } + + init { + setState { copy(isMine = session.myUserId == this.userId) } + observeIgnoredState() + viewModelScope.launch(Dispatchers.Main) { + // Do we have a room member for this id. + val roomMember = withContext(Dispatchers.Default) { + room?.getRoomMember(initialState.userId) + } + // If not, we look for profile info on the server + if (room == null || roomMember == null) { + fetchProfileInfo() + } else { + // otherwise we just start listening to db + setState { copy(showAsMember = true) } + observeRoomMemberSummary(room) + observeRoomSummaryAndPowerLevels(room) + } + } + } + + private fun observeIgnoredState() { + session.rx().liveIgnoredUsers() + .map { ignored -> + ignored.find { + it.userId == initialState.userId + } != null + } + .execute { + copy(isIgnored = it) + } + } + + override fun handle(action: RoomMemberProfileAction) { + when (action) { + RoomMemberProfileAction.RetryFetchingInfo -> fetchProfileInfo() + is RoomMemberProfileAction.IgnoreUser -> handleIgnoreAction() + } + } + + private fun observeRoomMemberSummary(room: Room) { + val queryParams = roomMemberQueryParams { + this.userId = QueryStringValue.Equals(initialState.userId, QueryStringValue.Case.SENSITIVE) + } + room.rx().liveRoomMembers(queryParams) + .map { it.firstOrNull()?.toMatrixItem().toOptional() } + .unwrap() + .execute { + copy(userMatrixItem = it) + } + } + + private fun fetchProfileInfo() { + session.rx().getProfileInfo(initialState.userId) + .map { + MatrixItem.UserItem( + id = initialState.userId, + displayName = it[ProfileService.DISPLAY_NAME_KEY] as? String, + avatarUrl = it[ProfileService.AVATAR_URL_KEY] as? String + ) + } + .execute { + copy(userMatrixItem = it) + } + } + + private fun observeRoomSummaryAndPowerLevels(room: Room) { + val roomSummaryLive = room.rx().liveRoomSummary().unwrap() + val powerLevelsContentLive = room.rx().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + .mapOptional { it.content.toModel() } + .unwrap() + + roomSummaryLive.execute { + copy(isRoomEncrypted = it.invoke()?.isEncrypted == true) + } + powerLevelsContentLive.execute { + copy(powerLevelsContent = it) + } + + Observable + .combineLatest( + roomSummaryLive, + powerLevelsContentLive, + BiFunction { roomSummary, powerLevelsContent -> + val roomName = roomSummary.toMatrixItem().getBestName() + val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) + val userPowerLevel = powerLevelsHelper.getUserPowerLevel(initialState.userId) + if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_ADMIN_LEVEL) { + stringProvider.getString(R.string.room_member_power_level_admin_in, roomName) + } else if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL) { + stringProvider.getString(R.string.room_member_power_level_moderator_in, roomName) + } else if (userPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL) { + "" + } else { + stringProvider.getString(R.string.room_member_power_level_custom_in, userPowerLevel, roomName) + } + } + ).execute { + copy(userPowerLevelString = it) + } + } + + private fun handleIgnoreAction() = withState { state -> + val isIgnored = state.isIgnored() ?: return@withState + _viewEvents.post(RoomMemberProfileViewEvents.Loading(stringProvider.getString(R.string.please_wait))) + val ignoreActionCallback = object : MatrixCallback { + override fun onSuccess(data: Unit) { + _viewEvents.post(RoomMemberProfileViewEvents.OnIgnoreActionSuccess) + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(RoomMemberProfileViewEvents.Failure(failure)) + } + } + if (isIgnored) { + session.unIgnoreUserIds(listOf(state.userId), ignoreActionCallback) + } else { + session.ignoreUserIds(listOf(state.userId), ignoreActionCallback) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt new file mode 100644 index 0000000000..ba079e9996 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roommemberprofile/RoomMemberProfileViewState.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package im.vector.riotx.features.roommemberprofile + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent +import im.vector.matrix.android.api.util.MatrixItem + +data class RoomMemberProfileViewState( + val userId: String, + val roomId: String?, + val showAsMember: Boolean = false, + val isMine: Boolean = false, + val isIgnored: Async = Uninitialized, + val isRoomEncrypted: Boolean = false, + val powerLevelsContent: Async = Uninitialized, + val userPowerLevelString: Async = Uninitialized, + val userMatrixItem: Async = Uninitialized +) : MvRxState { + + constructor(args: RoomMemberProfileArgs) : this(roomId = args.roomId, userId = args.userId) +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt new file mode 100644 index 0000000000..d3852e34c8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileAction.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile + +import im.vector.matrix.android.api.session.room.notification.RoomNotificationState +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class RoomProfileAction: VectorViewModelAction { + object LeaveRoom: RoomProfileAction() + data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt new file mode 100644 index 0000000000..160d2341ed --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileActivity.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile + +import android.content.Context +import android.content.Intent +import androidx.appcompat.widget.Toolbar +import im.vector.riotx.R +import im.vector.riotx.core.extensions.addFragment +import im.vector.riotx.core.extensions.addFragmentToBackstack +import im.vector.riotx.core.platform.ToolbarConfigurable +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment + +class RoomProfileActivity : VectorBaseActivity(), ToolbarConfigurable { + + companion object { + + private const val EXTRA_ROOM_PROFILE_ARGS = "EXTRA_ROOM_PROFILE_ARGS" + + fun newIntent(context: Context, roomId: String): Intent { + val roomProfileArgs = RoomProfileArgs(roomId) + return Intent(context, RoomProfileActivity::class.java).apply { + putExtra(EXTRA_ROOM_PROFILE_ARGS, roomProfileArgs) + } + } + } + + private lateinit var sharedActionViewModel: RoomProfileSharedActionViewModel + private lateinit var roomProfileArgs: RoomProfileArgs + + override fun getLayoutRes() = R.layout.activity_simple + + override fun initUiAndData() { + sharedActionViewModel = viewModelProvider.get(RoomProfileSharedActionViewModel::class.java) + roomProfileArgs = intent?.extras?.getParcelable(EXTRA_ROOM_PROFILE_ARGS) ?: return + if (isFirstCreation()) { + addFragment(R.id.simpleFragmentContainer, RoomProfileFragment::class.java, roomProfileArgs) + } + sharedActionViewModel + .observe() + .subscribe { sharedAction -> + when (sharedAction) { + is RoomProfileSharedAction.OpenRoomMembers -> openRoomMembers() + is RoomProfileSharedAction.OpenRoomSettings -> openRoomSettings() + is RoomProfileSharedAction.OpenRoomUploads -> openRoomUploads() + } + } + .disposeOnDestroy() + } + + private fun openRoomUploads() { + notImplemented("Open room uploads") + } + + private fun openRoomSettings() { + notImplemented("Open room settings") + } + + private fun openRoomMembers() { + addFragmentToBackstack(R.id.simpleFragmentContainer, RoomMemberListFragment::class.java, roomProfileArgs) + } + + override fun configure(toolbar: Toolbar) { + configureToolbar(toolbar) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileController.kt new file mode 100644 index 0000000000..37a36fbcf1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileController.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.riotx.R +import im.vector.riotx.core.epoxy.profiles.buildProfileAction +import im.vector.riotx.core.epoxy.profiles.buildProfileSection +import im.vector.riotx.core.resources.StringProvider +import javax.inject.Inject + +class RoomProfileController @Inject constructor(private val stringProvider: StringProvider) + : TypedEpoxyController() { + + var callback: Callback? = null + + interface Callback { + fun onLearnMoreClicked() + fun onMemberListClicked() + fun onNotificationsClicked() + fun onUploadsClicked() + fun onSettingsClicked() + fun onLeaveRoomClicked() + } + + override fun buildModels(data: RoomProfileViewState?) { + if (data == null) { + return + } + val roomSummary = data.roomSummary() ?: return + // Security + buildProfileSection(stringProvider.getString(R.string.room_profile_section_security)) + val learnMoreSubtitle = if (roomSummary.isEncrypted) { + R.string.room_profile_encrypted_subtitle + } else { + R.string.room_profile_not_encrypted_subtitle + } + buildProfileAction( + id = "learn_more", + title = stringProvider.getString(R.string.room_profile_section_security_learn_more), + subtitle = stringProvider.getString(learnMoreSubtitle), + action = { callback?.onLearnMoreClicked() } + ) + + // More + buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) + buildProfileAction( + id = "settings", + title = stringProvider.getString(R.string.room_profile_section_more_settings), + icon = R.drawable.ic_room_profile_settings, + action = { callback?.onSettingsClicked() } + ) + buildProfileAction( + id = "notifications", + title = stringProvider.getString(R.string.room_profile_section_more_notifications), + icon = R.drawable.ic_room_profile_notification, + action = { callback?.onNotificationsClicked() } + ) + val numberOfMembers = roomSummary.joinedMembersCount ?: 0 + buildProfileAction( + id = "member_list", + title = stringProvider.getQuantityString(R.plurals.room_profile_section_more_member_list, numberOfMembers, numberOfMembers), + icon = R.drawable.ic_room_profile_member_list, + action = { callback?.onMemberListClicked() } + ) + buildProfileAction( + id = "uploads", + title = stringProvider.getString(R.string.room_profile_section_more_uploads), + icon = R.drawable.ic_room_profile_uploads, + action = { callback?.onUploadsClicked() } + ) + buildProfileAction( + id = "leave", + title = stringProvider.getString(R.string.room_profile_section_more_leave), + divider = false, + destructive = true, + editable = false, + action = { callback?.onLeaveRoomClicked() } + ) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt new file mode 100644 index 0000000000..da161efd6a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileFragment.kt @@ -0,0 +1,184 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile + +import android.os.Bundle +import android.os.Parcelable +import android.view.View +import androidx.appcompat.app.AlertDialog +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.room.notification.RoomNotificationState +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.R +import im.vector.riotx.core.animations.AppBarStateChangeListener +import im.vector.riotx.core.animations.MatrixItemAppBarStateChangeListener +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.list.actions.RoomListActionsArgs +import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet +import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedAction +import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.fragment_matrix_profile.* +import kotlinx.android.synthetic.main.view_stub_room_profile_header.* +import timber.log.Timber +import javax.inject.Inject + +@Parcelize +data class RoomProfileArgs( + val roomId: String +) : Parcelable + +class RoomProfileFragment @Inject constructor( + private val roomProfileController: RoomProfileController, + private val avatarRenderer: AvatarRenderer, + val roomProfileViewModelFactory: RoomProfileViewModel.Factory +) : VectorBaseFragment(), RoomProfileController.Callback { + + private val roomProfileArgs: RoomProfileArgs by args() + private lateinit var roomListQuickActionsSharedActionViewModel: RoomListQuickActionsSharedActionViewModel + private lateinit var roomProfileSharedActionViewModel: RoomProfileSharedActionViewModel + private val roomProfileViewModel: RoomProfileViewModel by fragmentViewModel() + + private lateinit var appBarStateChangeListener: AppBarStateChangeListener + + override fun getLayoutResId() = R.layout.fragment_matrix_profile + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + roomListQuickActionsSharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) + roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java) + val headerView = matrixProfileHeaderView.let { + it.layoutResource = R.layout.view_stub_room_profile_header + it.inflate() + } + setupToolbar(matrixProfileToolbar) + setupRecyclerView() + appBarStateChangeListener = MatrixItemAppBarStateChangeListener(headerView, listOf(matrixProfileToolbarAvatarImageView, + matrixProfileToolbarTitleView)) + matrixProfileAppBarLayout.addOnOffsetChangedListener(appBarStateChangeListener) + roomProfileViewModel.viewEvents + .observe() + .subscribe { + dismissLoadingDialog() + when (it) { + is RoomProfileViewEvents.Loading -> showLoadingDialog(it.message) + RoomProfileViewEvents.OnLeaveRoomSuccess -> onLeaveRoom() + is RoomProfileViewEvents.Failure -> showError(it.throwable) + } + } + .disposeOnDestroyView() + roomListQuickActionsSharedActionViewModel + .observe() + .subscribe { handleQuickActions(it) } + .disposeOnDestroyView() + } + + private fun handleQuickActions(action: RoomListQuickActionsSharedAction) = when (action) { + is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> { + roomProfileViewModel.handle(RoomProfileAction.ChangeRoomNotificationState(RoomNotificationState.ALL_MESSAGES_NOISY)) + } + is RoomListQuickActionsSharedAction.NotificationsAll -> { + roomProfileViewModel.handle(RoomProfileAction.ChangeRoomNotificationState(RoomNotificationState.ALL_MESSAGES)) + } + is RoomListQuickActionsSharedAction.NotificationsMentionsOnly -> { + roomProfileViewModel.handle(RoomProfileAction.ChangeRoomNotificationState(RoomNotificationState.MENTIONS_ONLY)) + } + is RoomListQuickActionsSharedAction.NotificationsMute -> { + roomProfileViewModel.handle(RoomProfileAction.ChangeRoomNotificationState(RoomNotificationState.MUTE)) + } + else -> Timber.v("$action not handled") + } + + private fun onLeaveRoom() { + vectorBaseActivity.finish() + } + + private fun showError(throwable: Throwable) { + showErrorInSnackbar(throwable) + } + + private fun setupRecyclerView() { + roomProfileController.callback = this + matrixProfileRecyclerView.configureWith(roomProfileController, hasFixedSize = true) + } + + override fun onDestroyView() { + super.onDestroyView() + matrixProfileAppBarLayout.removeOnOffsetChangedListener(appBarStateChangeListener) + matrixProfileRecyclerView.cleanup() + } + + override fun invalidate() = withState(roomProfileViewModel) { state -> + state.roomSummary()?.also { + if (it.membership.isLeft()) { + Timber.w("The room has been left") + activity?.finish() + } else { + roomProfileNameView.text = it.displayName + matrixProfileToolbarTitleView.text = it.displayName + roomProfileAliasView.setTextOrHide(it.canonicalAlias) + roomProfileTopicView.setTextOrHide(it.topic) + val matrixItem = it.toMatrixItem() + avatarRenderer.render(matrixItem, roomProfileAvatarView) + avatarRenderer.render(matrixItem, matrixProfileToolbarAvatarImageView) + } + } + roomProfileController.setData(state) + } + + // RoomProfileController.Callback + + override fun onLearnMoreClicked() { + vectorBaseActivity.notImplemented() + } + + override fun onMemberListClicked() { + roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomMembers) + } + + override fun onSettingsClicked() { + roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomSettings) + } + + override fun onNotificationsClicked() { + RoomListQuickActionsBottomSheet + .newInstance(roomProfileArgs.roomId, RoomListActionsArgs.Mode.NOTIFICATIONS) + .show(childFragmentManager, "ROOM_PROFILE_NOTIFICATIONS") + } + + override fun onUploadsClicked() { + roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomUploads) + } + + override fun onLeaveRoomClicked() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.room_participants_leave_prompt_title) + .setMessage(R.string.room_participants_leave_prompt_msg) + .setPositiveButton(R.string.leave) { _, _ -> + roomProfileViewModel.handle(RoomProfileAction.LeaveRoom) + } + .setNegativeButton(R.string.cancel, null) + .show() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileSharedAction.kt new file mode 100644 index 0000000000..71142c0aae --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileSharedAction.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile + +import im.vector.riotx.core.platform.VectorSharedAction + +/** + * Supported navigation actions for [RoomProfileActivity] + */ +sealed class RoomProfileSharedAction : VectorSharedAction { + object OpenRoomSettings : RoomProfileSharedAction() + object OpenRoomUploads : RoomProfileSharedAction() + object OpenRoomMembers : RoomProfileSharedAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileSharedActionViewModel.kt new file mode 100644 index 0000000000..e1921b0c23 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileSharedActionViewModel.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile + +import im.vector.riotx.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +class RoomProfileSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt new file mode 100644 index 0000000000..3834247b52 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewEvents.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile +/** + * Transient events for RoomProfile + */ +sealed class RoomProfileViewEvents { + data class Loading(val message: CharSequence): RoomProfileViewEvents() + object OnLeaveRoomSuccess: RoomProfileViewEvents() + data class Failure(val throwable: Throwable) : RoomProfileViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt new file mode 100644 index 0000000000..8134c93b4f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewModel.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile + +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.unwrap +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.utils.DataSource +import im.vector.riotx.core.utils.PublishDataSource + +class RoomProfileViewModel @AssistedInject constructor(@Assisted initialState: RoomProfileViewState, + private val stringProvider: StringProvider, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RoomProfileViewState): RoomProfileViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomProfileViewState): RoomProfileViewModel? { + val fragment: RoomProfileFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.roomProfileViewModelFactory.create(state) + } + } + + private val _viewEvents = PublishDataSource() + val viewEvents: DataSource = _viewEvents + + private val room = session.getRoom(initialState.roomId)!! + + init { + observeRoomSummary() + } + + private fun observeRoomSummary() { + room.rx().liveRoomSummary() + .unwrap() + .execute { + copy(roomSummary = it) + } + } + + override fun handle(action: RoomProfileAction) = when (action) { + RoomProfileAction.LeaveRoom -> handleLeaveRoom() + is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) + } + + private fun handleChangeNotificationMode(action: RoomProfileAction.ChangeRoomNotificationState) { + room.setRoomNotificationState(action.notificationState, object : MatrixCallback { + override fun onFailure(failure: Throwable) { + _viewEvents.post(RoomProfileViewEvents.Failure(failure)) + } + }) + } + + private fun handleLeaveRoom() { + _viewEvents.post(RoomProfileViewEvents.Loading(stringProvider.getString(R.string.room_profile_leaving_room))) + room.leave(null, object : MatrixCallback { + override fun onSuccess(data: Unit) { + _viewEvents.post(RoomProfileViewEvents.OnLeaveRoomSuccess) + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(RoomProfileViewEvents.Failure(failure)) + } + }) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewState.kt new file mode 100644 index 0000000000..aed1488b07 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/RoomProfileViewState.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.room.model.RoomSummary + +data class RoomProfileViewState( + val roomId: String, + val roomSummary: Async = Uninitialized +) : MvRxState { + + constructor(args: RoomProfileArgs) : this(roomId = args.roomId) +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListAction.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListAction.kt new file mode 100644 index 0000000000..01a35b84d3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListAction.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile.members + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class RoomMemberListAction : VectorViewModelAction diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt new file mode 100644 index 0000000000..6b7ef74806 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListController.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile.members + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.core.epoxy.dividerItem +import im.vector.riotx.core.epoxy.profiles.buildProfileSection +import im.vector.riotx.core.epoxy.profiles.profileMatrixItem +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +class RoomMemberListController @Inject constructor(private val avatarRenderer: AvatarRenderer, + private val stringProvider: StringProvider) : TypedEpoxyController() { + + interface Callback { + fun onRoomMemberClicked(roomMember: RoomMemberSummary) + } + + var callback: Callback? = null + + init { + setData(null) + } + + override fun buildModels(data: RoomMemberListViewState?) { + val roomMembersByPowerLevel = data?.roomMemberSummaries?.invoke() ?: return + for ((powerLevelCategory, roomMemberList) in roomMembersByPowerLevel) { + if (roomMemberList.isEmpty()) { + continue + } + buildProfileSection( + stringProvider.getString(powerLevelCategory.titleRes) + ) + roomMemberList.forEach { roomMember -> + profileMatrixItem { + id(roomMember.userId) + matrixItem(roomMember.toMatrixItem()) + avatarRenderer(avatarRenderer) + clickListener { _ -> + callback?.onRoomMemberClicked(roomMember) + } + } + + dividerItem { + id("divider_${roomMember.userId}") + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt new file mode 100644 index 0000000000..4919fa39e3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListFragment.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile.members + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary +import im.vector.matrix.android.api.util.toMatrixItem +import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.roomprofile.RoomProfileArgs +import kotlinx.android.synthetic.main.fragment_room_member_list.* +import javax.inject.Inject + +class RoomMemberListFragment @Inject constructor( + val viewModelFactory: RoomMemberListViewModel.Factory, + private val roomMemberListController: RoomMemberListController, + private val avatarRenderer: AvatarRenderer +) : VectorBaseFragment(), RoomMemberListController.Callback { + + private val viewModel: RoomMemberListViewModel by fragmentViewModel() + private val roomProfileArgs: RoomProfileArgs by args() + + override fun getLayoutResId() = R.layout.fragment_room_member_list + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + roomMemberListController.callback = this + setupToolbar(roomMemberListToolbar) + recyclerView.configureWith(roomMemberListController, hasFixedSize = true) + } + + override fun onDestroyView() { + recyclerView.cleanup() + super.onDestroyView() + } + + override fun invalidate() = withState(viewModel) { viewState -> + roomMemberListController.setData(viewState) + renderRoomSummary(viewState) + } + + override fun onRoomMemberClicked(roomMember: RoomMemberSummary) { + navigator.openRoomMemberProfile(roomMember.userId, roomId = roomProfileArgs.roomId, context = requireActivity()) + } + + private fun renderRoomSummary(state: RoomMemberListViewState) { + state.roomSummary()?.let { + roomMemberListToolbarTitleView.text = it.displayName + avatarRenderer.render(it.toMatrixItem(), roomMemberListToolbarAvatarImageView) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt new file mode 100644 index 0000000000..c3db239047 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewModel.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile.members + +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.query.QueryStringValue +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.members.roomMemberQueryParams +import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.PowerLevelsContent +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants +import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper +import im.vector.matrix.rx.mapOptional +import im.vector.matrix.rx.rx +import im.vector.matrix.rx.unwrap +import im.vector.riotx.core.platform.VectorViewModel +import io.reactivex.Observable +import io.reactivex.functions.BiFunction + +class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomMemberListViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: RoomMemberListViewState): RoomMemberListViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: RoomMemberListViewState): RoomMemberListViewModel? { + val fragment: RoomMemberListFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } + + private val room = session.getRoom(initialState.roomId)!! + + init { + observeRoomMemberSummaries() + observeRoomSummary() + } + + private fun observeRoomMemberSummaries() { + val roomMemberQueryParams = roomMemberQueryParams { + displayName = QueryStringValue.IsNotEmpty + memberships = Membership.activeMemberships() + } + Observable + .combineLatest, PowerLevelsContent, RoomMemberSummaries>( + room.rx().liveRoomMembers(roomMemberQueryParams), + room.rx() + .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + .mapOptional { it.content.toModel() } + .unwrap(), + BiFunction { roomMembers, powerLevelsContent -> + buildRoomMemberSummaries(powerLevelsContent, roomMembers) + } + ) + .execute { async -> + copy(roomMemberSummaries = async) + } + } + + private fun observeRoomSummary() { + room.rx().liveRoomSummary() + .unwrap() + .execute { async -> + copy(roomSummary = async) + } + } + + private fun buildRoomMemberSummaries(powerLevelsContent: PowerLevelsContent, roomMembers: List): RoomMemberSummaries { + val admins = ArrayList() + val moderators = ArrayList() + val users = ArrayList(roomMembers.size) + val customs = ArrayList() + val invites = ArrayList() + val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) + roomMembers + .forEach { roomMember -> + val memberPowerLevel = powerLevelsHelper.getUserPowerLevel(roomMember.userId) + when { + roomMember.membership == Membership.INVITE -> invites.add(roomMember) + memberPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_ADMIN_LEVEL -> admins.add(roomMember) + memberPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_MODERATOR_LEVEL -> moderators.add(roomMember) + memberPowerLevel == PowerLevelsConstants.DEFAULT_ROOM_USER_LEVEL -> users.add(roomMember) + else -> customs.add(roomMember) + } + } + + return mapOf( + PowerLevelCategory.ADMIN to admins, + PowerLevelCategory.MODERATOR to moderators, + PowerLevelCategory.CUSTOM to customs, + PowerLevelCategory.INVITE to invites, + PowerLevelCategory.USER to users + ) + } + + override fun handle(action: RoomMemberListAction) { + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt new file mode 100644 index 0000000000..767de9a854 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/roomprofile/members/RoomMemberListViewState.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 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.riotx.features.roomprofile.members + +import androidx.annotation.StringRes +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.session.room.model.RoomMemberSummary +import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.riotx.R +import im.vector.riotx.features.roomprofile.RoomProfileArgs + +data class RoomMemberListViewState( + val roomId: String, + val roomSummary: Async = Uninitialized, + val roomMemberSummaries: Async = Uninitialized +) : MvRxState { + + constructor(args: RoomProfileArgs) : this(roomId = args.roomId) +} + +typealias RoomMemberSummaries = Map> + +enum class PowerLevelCategory(@StringRes val titleRes: Int) { + ADMIN(R.string.room_member_power_level_admins), + MODERATOR(R.string.room_member_power_level_moderators), + CUSTOM(R.string.room_member_power_level_custom), + INVITE(R.string.room_member_power_level_invites), + USER(R.string.room_member_power_level_users) +} diff --git a/vector/src/main/res/drawable/ic_room_profile_member_list.xml b/vector/src/main/res/drawable/ic_room_profile_member_list.xml new file mode 100644 index 0000000000..a80a0d4811 --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_profile_member_list.xml @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/vector/src/main/res/drawable/ic_room_profile_notification.xml b/vector/src/main/res/drawable/ic_room_profile_notification.xml new file mode 100644 index 0000000000..f2b500815d --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_profile_notification.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_room_profile_settings.xml b/vector/src/main/res/drawable/ic_room_profile_settings.xml new file mode 100644 index 0000000000..494ba69f14 --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_profile_settings.xml @@ -0,0 +1,30 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_room_profile_uploads.xml b/vector/src/main/res/drawable/ic_room_profile_uploads.xml new file mode 100644 index 0000000000..914a70f7fb --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_profile_uploads.xml @@ -0,0 +1,37 @@ + + + + + + diff --git a/vector/src/main/res/layout/activity_simple.xml b/vector/src/main/res/layout/activity_simple.xml index 8dc41487fe..0eda46a67d 100644 --- a/vector/src/main/res/layout/activity_simple.xml +++ b/vector/src/main/res/layout/activity_simple.xml @@ -1,5 +1,13 @@ - + android:layout_height="match_parent"> + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_matrix_profile.xml b/vector/src/main/res/layout/fragment_matrix_profile.xml new file mode 100644 index 0000000000..7bd6d8bfb0 --- /dev/null +++ b/vector/src/main/res/layout/fragment_matrix_profile.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 6661674edb..90455f0572 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -18,6 +18,7 @@ app:layout_constraintTop_toTopOf="parent"> diff --git a/vector/src/main/res/layout/fragment_room_member_list.xml b/vector/src/main/res/layout/fragment_room_member_list.xml new file mode 100644 index 0000000000..5fe4fc3770 --- /dev/null +++ b/vector/src/main/res/layout/fragment_room_member_list.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_bottom_sheet_divider.xml b/vector/src/main/res/layout/item_divider.xml similarity index 100% rename from vector/src/main/res/layout/item_bottom_sheet_divider.xml rename to vector/src/main/res/layout/item_divider.xml diff --git a/vector/src/main/res/layout/item_profile_action.xml b/vector/src/main/res/layout/item_profile_action.xml new file mode 100644 index 0000000000..d063025379 --- /dev/null +++ b/vector/src/main/res/layout/item_profile_action.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_profile_matrix_item.xml b/vector/src/main/res/layout/item_profile_matrix_item.xml new file mode 100644 index 0000000000..cdca2341e8 --- /dev/null +++ b/vector/src/main/res/layout/item_profile_matrix_item.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_profile_section.xml b/vector/src/main/res/layout/item_profile_section.xml new file mode 100644 index 0000000000..89efade46b --- /dev/null +++ b/vector/src/main/res/layout/item_profile_section.xml @@ -0,0 +1,17 @@ + + + + diff --git a/vector/src/main/res/layout/view_stub_room_member_profile_header.xml b/vector/src/main/res/layout/view_stub_room_member_profile_header.xml new file mode 100644 index 0000000000..82bf0e7d7b --- /dev/null +++ b/vector/src/main/res/layout/view_stub_room_member_profile_header.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_stub_room_profile_header.xml b/vector/src/main/res/layout/view_stub_room_profile_header.xml new file mode 100644 index 0000000000..6a0d0fc5de --- /dev/null +++ b/vector/src/main/res/layout/view_stub_room_profile_header.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/attr_behavior.xml b/vector/src/main/res/values/attr_behavior.xml new file mode 100644 index 0000000000..f974e16025 --- /dev/null +++ b/vector/src/main/res/values/attr_behavior.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 657e019ae3..3986d06f55 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -5,4 +5,34 @@ Your email domain is not authorized to register on this server + Messages in this room are not end-to-end encrypted. + Messages in this room are end-to-end encrypted. + Security + Learn more + More + Room settings + Notifications + + "One person" + "%1$d people" + + Uploads + Leave Room + "Leaving the room…" + + Admins + Moderators + Custom + Invites + Users + + Admin in %1$s + Moderator in %1$s + Custom (%1$d) in %2$s + + Jump to read receipt + + Unignore + + diff --git a/vector/src/test/java/im/vector/riotx/features/home/UserColorTest.kt b/vector/src/test/java/im/vector/riotx/features/home/UserColorTest.kt index e0795afb58..da8dba4137 100644 --- a/vector/src/test/java/im/vector/riotx/features/home/UserColorTest.kt +++ b/vector/src/test/java/im/vector/riotx/features/home/UserColorTest.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.home import im.vector.riotx.R +import im.vector.riotx.core.utils.getColorFromUserId import org.junit.Assert.assertEquals import org.junit.Test