diff --git a/CHANGES.md b/CHANGES.md index 9b4e022d5e..7ea78c2417 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,20 @@ +Changes in RiotX 0.10.0 (2019-12-10) +=================================================== + +Features ✨: + - Breadcrumbs: switch from one room to another quickly (#571) + +Improvements 🙌: + - Support entering a RiotWeb client URL instead of the homeserver URL during connection (#744) + +Other changes: + - Add reason for all membership events (https://github.com/matrix-org/matrix-doc/pull/2367) + +Bugfix 🐛: + - When automardown is ON, pills are sent as MD in body (#739) + - "ban" event are not rendered correctly (#716) + - Fix crash when rotating screen in Room timeline + Changes in RiotX 0.9.1 (2019-12-05) =================================================== diff --git a/docs/signup.md b/docs/signup.md index 7372ad2204..995f5d50a6 100644 --- a/docs/signup.md +++ b/docs/signup.md @@ -17,7 +17,7 @@ Client request the sign-up flows, once the homeserver is chosen by the user and } ``` -We get the flows with a 401, which also means the the registration is possible on this homeserver. +We get the flows with a 401, which also means that the registration is possible on this homeserver. ```json { 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 6793d6249d..bc0a866117 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 @@ -57,8 +57,9 @@ class RxRoom(private val room: Room) { room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it) } - fun joinRoom(viaServers: List = emptyList()): Single = Single.create { - room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it) + fun joinRoom(reason: String? = null, + viaServers: List = emptyList()): Single = Single.create { + room.join(reason, viaServers, MatrixCallbackSingle(it)).toSingle(it) } fun liveEventReadReceipts(eventId: String): Observable> { 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 1572851d3a..5a42dbb804 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 @@ -38,6 +38,10 @@ class RxSession(private val session: Session) { return session.liveGroupSummaries().asObservable() } + fun liveBreadcrumbs(): Observable> { + return session.liveBreadcrumbs().asObservable() + } + fun liveSyncState(): Observable { return session.syncState().asObservable() } @@ -72,8 +76,10 @@ class RxSession(private val session: Session) { session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it) } - fun joinRoom(roomId: String, viaServers: List = emptyList()): Single = Single.create { - session.joinRoom(roomId, viaServers, MatrixCallbackSingle(it)).toSingle(it) + fun joinRoom(roomId: String, + reason: String? = null, + viaServers: List = emptyList()): Single = Single.create { + session.joinRoom(roomId, reason, viaServers, MatrixCallbackSingle(it)).toSingle(it) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt index f0d0c61d58..dd0c93a41c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/LoginFlowResult.kt @@ -22,7 +22,8 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse sealed class LoginFlowResult { data class Success( val loginFlowResponse: LoginFlowResponse, - val isLoginAndRegistrationSupported: Boolean + val isLoginAndRegistrationSupported: Boolean, + val homeServerUrl: String ) : LoginFlowResult() object OutdatedHomeserver : LoginFlowResult() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt index 9d42e8388c..4d44e3346b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt @@ -36,7 +36,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString())) object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false"))) // When server send an error, but it cannot be interpreted as a MatrixError - data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody)) + data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody")) data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString())) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt index 540b164aa5..dedf9a83cf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt @@ -22,6 +22,8 @@ interface ContentUploadStateTracker { fun untrack(key: String, updateListener: UpdateListener) + fun clear() + interface UpdateListener { fun onUpdate(state: State) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt index 930320d976..c0e413f83b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt @@ -30,12 +30,16 @@ interface RoomDirectoryService { /** * Get rooms from directory */ - fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback): Cancelable + fun getPublicRooms(server: String?, + publicRoomsParams: PublicRoomsParams, + callback: MatrixCallback): Cancelable /** * Join a room by id */ - fun joinRoom(roomId: String, callback: MatrixCallback): Cancelable + fun joinRoom(roomId: String, + reason: String? = null, + callback: MatrixCallback): Cancelable /** * Fetches the overall metadata about protocols supported by the homeserver. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index c7fedb2627..98abce5898 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -30,14 +30,17 @@ interface RoomService { /** * Create a room asynchronously */ - fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable + fun createRoom(createRoomParams: CreateRoomParams, + callback: MatrixCallback): Cancelable /** * Join a room by id * @param roomId the roomId of the room to join + * @param reason optional reason for joining the room * @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room. */ fun joinRoom(roomId: String, + reason: String? = null, viaServers: List = emptyList(), callback: MatrixCallback): Cancelable @@ -54,8 +57,21 @@ interface RoomService { */ fun liveRoomSummaries(): LiveData> + /** + * Get a live list of Breadcrumbs + * @return the [LiveData] of [RoomSummary] + */ + fun liveBreadcrumbs(): LiveData> + + /** + * Inform the Matrix SDK that a room is displayed. + * The SDK will update the breadcrumbs in the user account data + */ + fun onRoomDisplayed(roomId: String): Cancelable + /** * Mark all rooms as read */ - fun markAllAsRead(roomIds: List, callback: MatrixCallback): Cancelable + fun markAllAsRead(roomIds: List, + callback: 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 8d60bee9da..b750c5347e 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 @@ -52,16 +52,21 @@ interface MembershipService { /** * Invite a user in the room */ - fun invite(userId: String, callback: MatrixCallback): Cancelable + fun invite(userId: String, + reason: String? = null, + callback: MatrixCallback): Cancelable /** * Join the room, or accept an invitation. */ - fun join(viaServers: List = emptyList(), callback: MatrixCallback): Cancelable + fun join(reason: String? = null, + viaServers: List = emptyList(), + callback: MatrixCallback): Cancelable /** * Leave the room, or reject an invitation. */ - fun leave(callback: MatrixCallback): Cancelable + fun leave(reason: String? = null, + callback: MatrixCallback): Cancelable } 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/RoomMember.kt index aa73727685..6a4d8e3c94 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/RoomMember.kt @@ -26,9 +26,13 @@ import im.vector.matrix.android.api.session.events.model.UnsignedData @JsonClass(generateAdapter = true) data class RoomMember( @Json(name = "membership") val membership: Membership, + @Json(name = "reason") val reason: String? = null, @Json(name = "displayname") val displayName: String? = null, @Json(name = "avatar_url") val avatarUrl: String? = null, @Json(name = "is_direct") val isDirect: Boolean = false, @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null, @Json(name = "unsigned") val unsignedData: UnsignedData? = null -) +) { + val safeReason + get() = reason?.takeIf { it.isNotBlank() } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt index 385699b4db..b3dd1c6f22 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -50,6 +50,7 @@ interface RelationService { /** * Sends a reaction (emoji) to the targetedEvent. + * It has no effect if the user has already added the same reaction to the event. * @param targetEventId the id of the event being reacted * @param reaction the reaction (preferably emoji) */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index a1c746a299..306a3846bc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Versions import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.PasswordLoginParams +import im.vector.matrix.android.internal.auth.data.RiotConfig import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed import im.vector.matrix.android.internal.auth.registration.* import im.vector.matrix.android.internal.network.NetworkConstants @@ -31,6 +32,12 @@ import retrofit2.http.* */ internal interface AuthAPI { + /** + * Get a Riot config file + */ + @GET("config.json") + fun getRiotConfig(): Call + /** * Get the version information of the homeserver */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt index e7cf999820..93349f4bbc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt @@ -16,16 +16,19 @@ package im.vector.matrix.android.internal.auth +import android.net.Uri import dagger.Lazy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.data.* import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard +import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.auth.data.LoginFlowResponse +import im.vector.matrix.android.internal.auth.data.RiotConfig import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard @@ -40,6 +43,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated private val okHttpClient: Lazy, @@ -84,7 +88,12 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated { if (it is LoginFlowResult.Success) { // The homeserver exists and up to date, keep the config - pendingSessionData = PendingSessionData(homeServerConnectionConfig) + // Homeserver url may have been changed, if it was a Riot url + val alteredHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(it.homeServerUrl) + ) + + pendingSessionData = PendingSessionData(alteredHomeServerConnectionConfig) .also { data -> pendingSessionStore.savePendingSessionData(data) } } callback.onSuccess(it) @@ -97,20 +106,71 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated .toCancelable() } - private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) { + private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { + return withContext(coroutineDispatchers.io) { + val authAPI = buildAuthAPI(homeServerConnectionConfig) + + // First check the homeserver version + runCatching { + executeRequest { + apiCall = authAPI.versions() + } + } + .map { versions -> + // Ok, it seems that the homeserver url is valid + getLoginFlowResult(authAPI, versions, homeServerConnectionConfig.homeServerUri.toString()) + } + .fold( + { + it + }, + { + if (it is Failure.OtherServerError + && it.httpCode == HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) { + // It's maybe a Riot url? + getRiotLoginFlowInternal(homeServerConnectionConfig) + } else { + throw it + } + } + ) + } + } + + private suspend fun getRiotLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult { val authAPI = buildAuthAPI(homeServerConnectionConfig) - // First check the homeserver version - val versions = executeRequest { - apiCall = authAPI.versions() + // Ok, try to get the config.json file of a RiotWeb client + val riotConfig = executeRequest { + apiCall = authAPI.getRiotConfig() } - if (versions.isSupportedBySdk()) { + if (riotConfig.defaultHomeServerUrl?.isNotBlank() == true) { + // Ok, good sign, we got a default hs url + val newHomeServerConnectionConfig = homeServerConnectionConfig.copy( + homeServerUri = Uri.parse(riotConfig.defaultHomeServerUrl) + ) + + val newAuthAPI = buildAuthAPI(newHomeServerConnectionConfig) + + val versions = executeRequest { + apiCall = newAuthAPI.versions() + } + + return getLoginFlowResult(newAuthAPI, versions, riotConfig.defaultHomeServerUrl) + } else { + // Config exists, but there is no default homeserver url (ex: https://riot.im/app) + throw Failure.OtherServerError("", HttpsURLConnection.HTTP_NOT_FOUND /* 404 */) + } + } + + private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { + return if (versions.isSupportedBySdk()) { // Get the login flow val loginFlowResponse = executeRequest { apiCall = authAPI.getLoginFlows() } - LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk()) + LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl) } else { // Not supported LoginFlowResult.OutdatedHomeserver diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/RiotConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/RiotConfig.kt new file mode 100644 index 0000000000..aebcfe0305 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/RiotConfig.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.matrix.android.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RiotConfig( + // There are plenty of other elements in the file config.json of a RiotWeb client, but for the moment only one is interesting + // Ex: "brand", "branding", etc. + @Json(name = "default_hs_url") + val defaultHomeServerUrl: String? +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt index d7ab87a401..1e902f5133 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt @@ -19,16 +19,14 @@ package im.vector.matrix.android.internal.crypto.store.db.query import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields import io.realm.Realm +import io.realm.kotlin.createObject import io.realm.kotlin.where /** * Get or create a room */ internal fun CryptoRoomEntity.Companion.getOrCreate(realm: Realm, roomId: String): CryptoRoomEntity { - return getById(realm, roomId) - ?: let { - realm.createObject(CryptoRoomEntity::class.java, roomId) - } + return getById(realm, roomId) ?: realm.createObject(roomId) } /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt index 706815ff76..08bbb8f920 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt @@ -20,18 +20,20 @@ import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey import io.realm.Realm +import io.realm.kotlin.createObject import io.realm.kotlin.where /** * Get or create a device info */ internal fun DeviceInfoEntity.Companion.getOrCreate(realm: Realm, userId: String, deviceId: String): DeviceInfoEntity { + val key = DeviceInfoEntity.createPrimaryKey(userId, deviceId) + return realm.where() - .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) + .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, key) .findFirst() - ?: let { - realm.createObject(DeviceInfoEntity::class.java, DeviceInfoEntity.createPrimaryKey(userId, deviceId)).apply { - this.deviceId = deviceId - } - } + ?: realm.createObject(key) + .apply { + this.deviceId = deviceId + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/UserEntitiesQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/UserEntitiesQueries.kt index 8088a14825..b7f75cfead 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/UserEntitiesQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/UserEntitiesQueries.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.store.db.query import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields import io.realm.Realm +import io.realm.kotlin.createObject import io.realm.kotlin.where /** @@ -28,9 +29,7 @@ internal fun UserEntity.Companion.getOrCreate(realm: Realm, userId: String): Use return realm.where() .equalTo(UserEntityFields.USER_ID, userId) .findFirst() - ?: let { - realm.createObject(UserEntity::class.java, userId) - } + ?: realm.createObject(userId) } /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/BreadcrumbsEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/BreadcrumbsEntity.kt new file mode 100644 index 0000000000..c396795421 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/BreadcrumbsEntity.kt @@ -0,0 +1,27 @@ +/* + * 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.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +internal open class BreadcrumbsEntity( + var recentRoomIds: RealmList = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index f414325aed..47904380a0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -38,7 +38,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var readMarkerId: String? = null, var hasUnreadMessages: Boolean = false, var tags: RealmList = RealmList(), - var userDrafts: UserDraftsEntity? = null + var userDrafts: UserDraftsEntity? = null, + var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS ) : RealmObject() { private var membershipStr: String = Membership.NONE.name @@ -59,5 +60,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", versioningStateStr = value.name } - companion object + companion object { + const val NOT_IN_BREADCRUMBS = -1 + } } 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 76b355b064..6059d3faf7 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 @@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule SyncEntity::class, UserEntity::class, IgnoredUserEntity::class, + BreadcrumbsEntity::class, EventAnnotationsSummaryEntity::class, ReactionAggregatedSummaryEntity::class, EditAggregatedSummaryEntity::class, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/BreadcrumbsEntityQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/BreadcrumbsEntityQuery.kt new file mode 100644 index 0000000000..60ed8aae7c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/BreadcrumbsEntityQuery.kt @@ -0,0 +1,30 @@ +/* + * 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.internal.database.query + +import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where + +internal fun BreadcrumbsEntity.Companion.get(realm: Realm): BreadcrumbsEntity? { + return realm.where().findFirst() +} + +internal fun BreadcrumbsEntity.Companion.getOrCreate(realm: Realm): BreadcrumbsEntity { + return get(realm) ?: realm.createObject() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt index d95dc58574..5f96c12953 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadMarkerEntityQueries.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadMarkerEntity import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields import io.realm.Realm import io.realm.RealmQuery +import io.realm.kotlin.createObject import io.realm.kotlin.where internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery { @@ -28,6 +29,5 @@ internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): Rea } internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity { - return where(realm, roomId).findFirst() - ?: realm.createObject(ReadMarkerEntity::class.java, roomId) + return where(realm, roomId).findFirst() ?: realm.createObject(roomId) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt index 6b996d1285..2aa40dd2a1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReadReceiptEntityQueries.kt @@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields import io.realm.Realm import io.realm.RealmQuery +import io.realm.kotlin.createObject import io.realm.kotlin.where internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery { @@ -44,10 +45,11 @@ internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity { return ReadReceiptEntity.where(realm, roomId, userId).findFirst() - ?: realm.createObject(ReadReceiptEntity::class.java, buildPrimaryKey(roomId, userId)).apply { - this.roomId = roomId - this.userId = userId - } + ?: realm.createObject(buildPrimaryKey(roomId, userId)) + .apply { + this.roomId = roomId + this.userId = userId + } } private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt index a92d81b54c..79473f3b10 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import io.realm.Realm import io.realm.RealmQuery import io.realm.RealmResults +import io.realm.kotlin.createObject import io.realm.kotlin.where internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery { @@ -32,8 +33,7 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n } internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity { - return where(realm, roomId).findFirst() - ?: realm.createObject(RoomSummaryEntity::class.java, roomId) + return where(realm, roomId).findFirst() ?: realm.createObject(roomId) } internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt index 96cdf29226..793be10880 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData +import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataBreadcrumbs import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers @@ -34,6 +35,7 @@ object MoshiProvider { .registerSubtype(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES) .registerSubtype(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST) .registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES) + .registerSubtype(UserAccountDataBreadcrumbs::class.java, UserAccountData.TYPE_BREADCRUMBS) ) .add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java) .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt index 29b20f9739..fa0b9a1f1c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.network import com.squareup.moshi.JsonDataException +import com.squareup.moshi.JsonEncodingException import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError @@ -106,6 +107,9 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure { } catch (ex: JsonDataException) { // This is not a MatrixError Timber.w("The error returned by the server is not a MatrixError") + } catch (ex: JsonEncodingException) { + // This is not a MatrixError, HTML code? + Timber.w("The error returned by the server is not a MatrixError, probably HTML string") } return Failure.OtherServerError(errorBodyStr, httpCode) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt index 68f48d20db..66a8341801 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt @@ -42,6 +42,10 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU } } + override fun clear() { + listeners.clear() + } + internal fun setFailure(key: String, throwable: Throwable) { val failure = ContentUploadStateTracker.State.Failure(throwable) updateState(key, failure) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt index 4251a66304..711e2bd97c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt @@ -44,9 +44,9 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu .executeBy(taskExecutor) } - override fun joinRoom(roomId: String, callback: MatrixCallback): Cancelable { + override fun joinRoom(roomId: String, reason: String?, callback: MatrixCallback): Cancelable { return joinRoomTask - .configureWith(JoinRoomTask.Params(roomId)) { + .configureWith(JoinRoomTask.Params(roomId, reason)) { this.callback = callback } .executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 962b7b54d6..22caf76eaf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -33,6 +33,7 @@ import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask +import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import io.realm.Realm @@ -43,6 +44,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona private val createRoomTask: CreateRoomTask, private val joinRoomTask: JoinRoomTask, private val markAllRoomsReadTask: MarkAllRoomsReadTask, + private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, private val roomFactory: RoomFactory, private val taskExecutor: TaskExecutor) : RoomService { @@ -75,9 +77,28 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona ) } - override fun joinRoom(roomId: String, viaServers: List, callback: MatrixCallback): Cancelable { + override fun liveBreadcrumbs(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm -> + RoomSummaryEntity.where(realm) + .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) + .notEqualTo(RoomSummaryEntityFields.VERSIONING_STATE_STR, VersioningState.UPGRADED_ROOM_JOINED.name) + .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS) + .sort(RoomSummaryEntityFields.BREADCRUMBS_INDEX) + }, + { roomSummaryMapper.map(it) } + ) + } + + override fun onRoomDisplayed(roomId: String): Cancelable { + return updateBreadcrumbsTask + .configureWith(UpdateBreadcrumbsTask.Params(roomId)) + .executeBy(taskExecutor) + } + + override fun joinRoom(roomId: String, reason: String?, viaServers: List, callback: MatrixCallback): Cancelable { return joinRoomTask - .configureWith(JoinRoomTask.Params(roomId, viaServers)) { + .configureWith(JoinRoomTask.Params(roomId, reason, viaServers)) { this.callback = callback } .executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index 797dbed31c..40164d1697 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -217,7 +217,7 @@ internal interface RoomAPI { @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join") fun join(@Path("roomId") roomId: String, @Query("server_name") viaServers: List, - @Body params: Map): Call + @Body params: Map): Call /** * Leave the given room. @@ -227,7 +227,7 @@ internal interface RoomAPI { */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave") fun leave(@Path("roomId") roomId: String, - @Body params: Map): Call + @Body params: Map): Call /** * Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room. 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 3490fed30f..00c1c2c4ca 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 @@ -83,8 +83,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr return result } - override fun invite(userId: String, callback: MatrixCallback): Cancelable { - val params = InviteTask.Params(roomId, userId) + override fun invite(userId: String, reason: String?, callback: MatrixCallback): Cancelable { + val params = InviteTask.Params(roomId, userId, reason) return inviteTask .configureWith(params) { this.callback = callback @@ -92,8 +92,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr .executeBy(taskExecutor) } - override fun join(viaServers: List, callback: MatrixCallback): Cancelable { - val params = JoinRoomTask.Params(roomId, viaServers) + override fun join(reason: String?, viaServers: List, callback: MatrixCallback): Cancelable { + val params = JoinRoomTask.Params(roomId, reason, viaServers) return joinTask .configureWith(params) { this.callback = callback @@ -101,8 +101,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr .executeBy(taskExecutor) } - override fun leave(callback: MatrixCallback): Cancelable { - val params = LeaveRoomTask.Params(roomId) + override fun leave(reason: String?, callback: MatrixCallback): Cancelable { + val params = LeaveRoomTask.Params(roomId, reason) return leaveRoomTask .configureWith(params) { this.callback = callback diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteBody.kt index 4529a17ab8..2d72197198 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteBody.kt @@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class InviteBody( - @Json(name = "user_id") val userId: String + @Json(name = "user_id") val userId: String, + @Json(name = "reason") val reason: String? ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt index a41e8d3ca3..6bc453a0f3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt @@ -24,7 +24,8 @@ import javax.inject.Inject internal interface InviteTask : Task { data class Params( val roomId: String, - val userId: String + val userId: String, + val reason: String? ) } @@ -32,7 +33,7 @@ internal class DefaultInviteTask @Inject constructor(private val roomAPI: RoomAP override suspend fun execute(params: InviteTask.Params) { return executeRequest { - val body = InviteBody(params.userId) + val body = InviteBody(params.userId, params.reason) apiCall = roomAPI.invite(params.roomId, body) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt index 2555d80209..7304c09d57 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt @@ -32,6 +32,7 @@ import javax.inject.Inject internal interface JoinRoomTask : Task { data class Params( val roomId: String, + val reason: String?, val viaServers: List = emptyList() ) } @@ -43,7 +44,7 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room override suspend fun execute(params: JoinRoomTask.Params) { executeRequest { - apiCall = roomAPI.join(params.roomId, params.viaServers, HashMap()) + apiCall = roomAPI.join(params.roomId, params.viaServers, mapOf("reason" to params.reason)) } val roomId = params.roomId // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt index be9a421e95..01198c47de 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt @@ -23,7 +23,8 @@ import javax.inject.Inject internal interface LeaveRoomTask : Task { data class Params( - val roomId: String + val roomId: String, + val reason: String? ) } @@ -31,7 +32,7 @@ internal class DefaultLeaveRoomTask @Inject constructor(private val roomAPI: Roo override suspend fun execute(params: LeaveRoomTask.Params) { return executeRequest { - apiCall = roomAPI.leave(params.roomId, HashMap()) + apiCall = roomAPI.leave(params.roomId, mapOf("reason" to params.reason)) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index db3b6100a0..8731045e14 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -30,10 +30,13 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.api.util.NoOpCancellable import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.toOptional +import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity 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.send.EncryptEventWorker @@ -44,6 +47,7 @@ import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEvent import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.CancelableWork +import im.vector.matrix.android.internal.util.fetchCopyMap import im.vector.matrix.android.internal.worker.WorkerParamsFactory import timber.log.Timber @@ -54,6 +58,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv private val cryptoService: CryptoService, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val fetchEditHistoryTask: FetchEditHistoryTask, + private val timelineEventMapper: TimelineEventMapper, private val monarchy: Monarchy, private val taskExecutor: TaskExecutor) : RelationService { @@ -64,11 +69,27 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv } override fun sendReaction(targetEventId: String, reaction: String): Cancelable { - val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) - .also { saveLocalEcho(it) } - val sendRelationWork = createSendEventWork(event, true) - TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork) - return CancelableWork(context, sendRelationWork.id) + return if (monarchy + .fetchCopyMap( + { realm -> + TimelineEventEntity.where(realm, roomId, targetEventId).findFirst() + }, + { entity, _ -> + timelineEventMapper.map(entity) + }) + ?.annotations + ?.reactionsSummary + .orEmpty() + .none { it.addedByMe && it.key == reaction }) { + val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) + .also { saveLocalEcho(it) } + val sendRelationWork = createSendEventWork(event, true) + TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork) + CancelableWork(context, sendRelationWork.id) + } else { + Timber.w("Reaction already added") + NoOpCancellable + } } override fun undoReaction(targetEventId: String, reaction: String): Cancelable { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index b773d1f892..0fed1ca6f5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -78,7 +78,7 @@ internal class LocalEchoEventFactory @Inject constructor( val htmlText = renderer.render(document) if (isFormattedTextPertinent(source, htmlText)) { - return TextContent(source, htmlText) + return TextContent(text.toString(), htmlText) } } else { // Try to detect pills diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index b83240a681..693855edbc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -289,6 +289,9 @@ internal class DefaultTimeline( } override fun addListener(listener: Timeline.Listener) = synchronized(listeners) { + if (listeners.contains(listener)) { + return false + } listeners.add(listener).also { postSnapshot() } @@ -494,9 +497,9 @@ internal class DefaultTimeline( return } val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask @@ -571,7 +574,7 @@ internal class DefaultTimeline( val timelineEvent = buildTimelineEvent(eventEntity) if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + && timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } } 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 56bc005805..9cc3a5a3c6 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 @@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers 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 +import im.vector.matrix.android.internal.session.user.accountdata.SaveBreadcrumbsTask import im.vector.matrix.android.internal.session.user.accountdata.SaveIgnoredUsersTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.task.TaskExecutor @@ -44,6 +45,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc private val updateUserAccountDataTask: UpdateUserAccountDataTask, private val savePushRulesTask: SavePushRulesTask, private val saveIgnoredUsersTask: SaveIgnoredUsersTask, + private val saveBreadcrumbsTask: SaveBreadcrumbsTask, private val taskExecutor: TaskExecutor) { suspend fun handle(accountData: UserAccountDataSync?, invites: Map?) { @@ -52,6 +54,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc is UserAccountDataDirectMessages -> handleDirectChatRooms(it) is UserAccountDataPushRules -> handlePushRules(it) is UserAccountDataIgnoredUsers -> handleIgnoredUsers(it) + is UserAccountDataBreadcrumbs -> handleBreadcrumbs(it) is UserAccountDataFallback -> Timber.d("Receive account data of unhandled type ${it.type}") else -> error("Missing code here!") } @@ -130,4 +133,10 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc .executeBy(taskExecutor) // TODO If not initial sync, we should execute a init sync } + + private fun handleBreadcrumbs(userAccountDataBreadcrumbs: UserAccountDataBreadcrumbs) { + saveBreadcrumbsTask + .configureWith(SaveBreadcrumbsTask.Params(userAccountDataBreadcrumbs.content.recentRoomIds)) + .executeBy(taskExecutor) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt index 55dbad6099..accc9c900f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountData.kt @@ -25,6 +25,7 @@ internal abstract class UserAccountData { companion object { const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list" const val TYPE_DIRECT_MESSAGES = "m.direct" + const val TYPE_BREADCRUMBS = "im.vector.setting.breadcrumbs" // Was previously "im.vector.riot.breadcrumb_rooms" const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls" const val TYPE_WIDGETS = "m.widgets" const val TYPE_PUSH_RULES = "m.push_rules" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataBreadcrumbs.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataBreadcrumbs.kt new file mode 100644 index 0000000000..cf5ee9c5e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/accountdata/UserAccountDataBreadcrumbs.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.matrix.android.internal.session.sync.model.accountdata + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class UserAccountDataBreadcrumbs( + @Json(name = "type") override val type: String = TYPE_BREADCRUMBS, + @Json(name = "content") val content: BreadcrumbsContent +) : UserAccountData() + +@JsonClass(generateAdapter = true) +internal data class BreadcrumbsContent( + @Json(name = "recent_rooms") val recentRoomIds: List = emptyList() +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt index 49fe8caf8e..1fd4162d0a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/AccountDataModule.kt @@ -35,5 +35,11 @@ internal abstract class AccountDataModule { } @Binds - abstract fun bindUpdateUserAccountDataTask(updateUserAccountDataTask: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask + abstract fun bindUpdateUserAccountDataTask(task: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask + + @Binds + abstract fun bindSaveBreadcrumbsTask(task: DefaultSaveBreadcrumbsTask): SaveBreadcrumbsTask + + @Binds + abstract fun bindUpdateBreadcrumsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/SaveBreadcrumbsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/SaveBreadcrumbsTask.kt new file mode 100644 index 0000000000..008dd1d652 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/SaveBreadcrumbsTask.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.matrix.android.internal.session.user.accountdata + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntity +import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields +import im.vector.matrix.android.internal.database.query.getOrCreate +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.awaitTransaction +import io.realm.RealmList +import javax.inject.Inject + +/** + * Save the Breadcrumbs roomId list in DB, either from the sync, or updated locally + */ +internal interface SaveBreadcrumbsTask : Task { + data class Params( + val recentRoomIds: List + ) +} + +internal class DefaultSaveBreadcrumbsTask @Inject constructor( + private val monarchy: Monarchy +) : SaveBreadcrumbsTask { + + override suspend fun execute(params: SaveBreadcrumbsTask.Params) { + monarchy.awaitTransaction { realm -> + // Get or create a breadcrumbs entity + val entity = BreadcrumbsEntity.getOrCreate(realm) + + // And save the new received list + entity.recentRoomIds = RealmList().apply { addAll(params.recentRoomIds) } + + // Update the room summaries + // Reset all the indexes... + RoomSummaryEntity.where(realm) + .greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS) + .findAll() + .forEach { + it.breadcrumbsIndex = RoomSummaryEntity.NOT_IN_BREADCRUMBS + } + + // ...and apply new indexes + params.recentRoomIds.forEachIndexed { index, roomId -> + RoomSummaryEntity.where(realm, roomId) + .findFirst() + ?.breadcrumbsIndex = index + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt new file mode 100644 index 0000000000..b11072a0bd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateBreadcrumbsTask.kt @@ -0,0 +1,66 @@ +/* + * 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.internal.session.user.accountdata + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity +import im.vector.matrix.android.internal.database.query.get +import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.fetchCopied +import javax.inject.Inject + +// Use the same arbitrary value than Riot-Web +private const val MAX_BREADCRUMBS_ROOMS_NUMBER = 20 + +internal interface UpdateBreadcrumbsTask : Task { + data class Params( + val newTopRoomId: String + ) +} + +internal class DefaultUpdateBreadcrumbsTask @Inject constructor( + private val saveBreadcrumbsTask: SaveBreadcrumbsTask, + private val updateUserAccountDataTask: UpdateUserAccountDataTask, + private val monarchy: Monarchy +) : UpdateBreadcrumbsTask { + + override suspend fun execute(params: UpdateBreadcrumbsTask.Params) { + val newBreadcrumbs = + // Get the breadcrumbs entity, if any + monarchy.fetchCopied { BreadcrumbsEntity.get(it) } + ?.recentRoomIds + ?.apply { + // Modify the list to add the newTopRoomId first + // Ensure the newTopRoomId is not already in the list + remove(params.newTopRoomId) + // Add the newTopRoomId at first position + add(0, params.newTopRoomId) + } + ?.take(MAX_BREADCRUMBS_ROOMS_NUMBER) + ?: listOf(params.newTopRoomId) + + // Update the DB locally, do not wait for the sync + saveBreadcrumbsTask.execute(SaveBreadcrumbsTask.Params(newBreadcrumbs)) + + // FIXME It can remove the previous breadcrumbs, if not synced yet + // And update account data + updateUserAccountDataTask.execute(UpdateUserAccountDataTask.BreadcrumbsParams( + breadcrumbsContent = BreadcrumbsContent(newBreadcrumbs) + )) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt index 9fa71005ff..4c4f40add5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.user.accountdata import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.internal.task.Task import javax.inject.Inject @@ -38,6 +39,15 @@ internal interface UpdateUserAccountDataTask : Task - - + %1$s\'s invitation. Reason: %2$s + %1$s invited %2$s. Reason: %3$s + %1$s invited you. Reason: %2$s + %1$s joined. Reason: %2$s + %1$s left. Reason: %2$s + %1$s rejected the invitation. Reason: %2$s + %1$s kicked %2$s. Reason: %3$s + %1$s unbanned %2$s. Reason: %3$s + %1$s banned %2$s. Reason: %3$s + %1$s sent an invitation to %2$s to join the room. Reason: %3$s + %1$s revoked the invitation for %2$s to join the room. Reason: %3$s + %1$s accepted the invitation for %2$s. Reason: %3$s + %1$s withdrew %2$s\'s invitation. Reason: %3$s There is no network connection right now \ No newline at end of file diff --git a/tools/check/forbidden_strings_in_resources.txt b/tools/check/forbidden_strings_in_resources.txt index 435a81e3ba..17ae015265 100644 --- a/tools/check/forbidden_strings_in_resources.txt +++ b/tools/check/forbidden_strings_in_resources.txt @@ -81,4 +81,7 @@ layout_constraintLeft_ ### Will crash on API < 21. Use ?colorAccent instead \?android:colorAccent -\?android:attr/colorAccent \ No newline at end of file +\?android:attr/colorAccent + +### Use androidx.recyclerview.widget.RecyclerView because EpoxyRecyclerViews add behavior we do not want to + { + when (throwable.httpCode) { + HttpURLConnection.HTTP_NOT_FOUND -> + // homeserver not found + stringProvider.getString(R.string.login_error_no_homeserver_found) + else -> + throwable.localizedMessage + } + } else -> throwable.localizedMessage } ?: stringProvider.getString(R.string.unknown_error) diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.kt new file mode 100644 index 0000000000..003045af51 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/extensions/RecyclerView.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.core.extensions + +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.EpoxyController + +/** + * Apply a Vertical LinearLayout Manager to the recyclerView and set the adapter from the epoxy controller + */ +fun RecyclerView.configureWith(epoxyController: EpoxyController, + itemAnimator: RecyclerView.ItemAnimator? = null, + showDivider: Boolean = false, + hasFixedSize: Boolean = true) { + layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + itemAnimator?.let { this.itemAnimator = it } + if (showDivider) { + addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + } + setHasFixedSize(hasFixedSize) + adapter = epoxyController.adapter +} + +/** + * To call from Fragment.onDestroyView() + */ +fun RecyclerView.cleanup() { + adapter = null +} 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 247d6f54ee..f674478724 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 @@ -51,7 +51,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet? init { View.inflate(context, R.layout.view_state, this) - layoutParams = LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) errorRetryView.setOnClickListener { eventCallback?.onRetryClicked() } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt index 5727580653..70311e2f57 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -73,7 +73,7 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() injectWith(screenComponent) } - protected open fun injectWith(screenComponent: ScreenComponent) = Unit + protected open fun injectWith(injector: ScreenComponent) = Unit override fun onCreate(savedInstanceState: Bundle?) { mvrxViewIdProperty.restoreFrom(savedInstanceState) 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 9f94c15edd..924cb6c7bc 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 @@ -135,6 +135,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) restorables.forEach { it.onSaveInstanceState(outState) } + restorables.clear() } override fun onViewStateRestored(savedInstanceState: Bundle?) { diff --git a/vector/src/main/java/im/vector/riotx/core/rx/Rx.kt b/vector/src/main/java/im/vector/riotx/core/rx/Rx.kt new file mode 100644 index 0000000000..89de9030dc --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/rx/Rx.kt @@ -0,0 +1,35 @@ +/* + * 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.rx + +import im.vector.riotx.BuildConfig +import io.reactivex.plugins.RxJavaPlugins +import timber.log.Timber + +/** + * Make sure unhandled Rx error does not crash the app in production + */ +fun setupRxPlugin() { + RxJavaPlugins.setErrorHandler { throwable -> + Timber.e(throwable, "RxError") + + // Avoid crash in production + if (BuildConfig.DEBUG) { + throw throwable + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt b/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt index a5babcc885..f9e5654726 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/Emoji.kt @@ -16,13 +16,6 @@ package im.vector.riotx.core.utils -import android.content.Context -import com.squareup.moshi.Moshi -import im.vector.riotx.R -import im.vector.riotx.features.reactions.EmojiDataSource -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import timber.log.Timber import java.util.regex.Pattern private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" + @@ -49,6 +42,7 @@ private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" + "|\uD83C\uDCCF\uFE0F?" + "|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))") +/* // A hashset from all supported emoji private var knownEmojiSet: HashSet? = null @@ -56,7 +50,7 @@ fun initKnownEmojiHashSet(context: Context, done: (() -> Unit)? = null) { GlobalScope.launch { context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input -> val moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(EmojiDataSource.EmojiData::class.java) + val jsonAdapter = moshi.adapter(EmojiData::class.java) val inputAsString = input.bufferedReader().use { it.readText() } val source = jsonAdapter.fromJson(inputAsString) knownEmojiSet = HashSet().also { @@ -77,6 +71,7 @@ fun isSingleEmoji(string: String): Boolean { } return knownEmojiSet?.contains(string) ?: false } + */ /** * Test if a string contains emojis. diff --git a/vector/src/main/java/im/vector/riotx/features/command/Command.kt b/vector/src/main/java/im/vector/riotx/features/command/Command.kt index 7d745b925b..8b72ffa4a6 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/Command.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/Command.kt @@ -27,16 +27,19 @@ import im.vector.riotx.R enum class Command(val command: String, val parameters: String, @StringRes val description: Int) { EMOTE("/me", "", R.string.command_description_emote), BAN_USER("/ban", " [reason]", R.string.command_description_ban_user), - UNBAN_USER("/unban", "", R.string.command_description_unban_user), + UNBAN_USER("/unban", " [reason]", R.string.command_description_unban_user), SET_USER_POWER_LEVEL("/op", " []", R.string.command_description_op_user), RESET_USER_POWER_LEVEL("/deop", "", R.string.command_description_deop_user), - INVITE("/invite", "", R.string.command_description_invite_user), - JOIN_ROOM("/join", "", R.string.command_description_join_room), - PART("/part", "", R.string.command_description_part_room), + INVITE("/invite", " [reason]", R.string.command_description_invite_user), + JOIN_ROOM("/join", " [reason]", R.string.command_description_join_room), + PART("/part", " [reason]", R.string.command_description_part_room), TOPIC("/topic", "", R.string.command_description_topic), KICK_USER("/kick", " [reason]", R.string.command_description_kick_user), CHANGE_DISPLAY_NAME("/nick", "", R.string.command_description_nick), MARKDOWN("/markdown", "", R.string.command_description_markdown), CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token), SPOILER("/spoiler", "", R.string.command_description_spoiler); + + val length + get() = command.length + 1 } diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt index bc451f8e84..359f2c1f13 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt @@ -81,29 +81,52 @@ object CommandParser { ParsedCommand.SendEmote(message) } Command.JOIN_ROOM.command -> { - val roomAlias = textMessage.substring(Command.JOIN_ROOM.command.length).trim() + if (messageParts.size >= 2) { + val roomAlias = messageParts[1] - if (roomAlias.isNotEmpty()) { - ParsedCommand.JoinRoom(roomAlias) + if (roomAlias.isNotEmpty()) { + ParsedCommand.JoinRoom( + roomAlias, + textMessage.substring(Command.JOIN_ROOM.length + roomAlias.length) + .trim() + .takeIf { it.isNotBlank() } + ) + } else { + ParsedCommand.ErrorSyntax(Command.JOIN_ROOM) + } } else { ParsedCommand.ErrorSyntax(Command.JOIN_ROOM) } } Command.PART.command -> { - val roomAlias = textMessage.substring(Command.PART.command.length).trim() + if (messageParts.size >= 2) { + val roomAlias = messageParts[1] - if (roomAlias.isNotEmpty()) { - ParsedCommand.PartRoom(roomAlias) + if (roomAlias.isNotEmpty()) { + ParsedCommand.PartRoom( + roomAlias, + textMessage.substring(Command.PART.length + roomAlias.length) + .trim() + .takeIf { it.isNotBlank() } + ) + } else { + ParsedCommand.ErrorSyntax(Command.PART) + } } else { ParsedCommand.ErrorSyntax(Command.PART) } } Command.INVITE.command -> { - if (messageParts.size == 2) { + if (messageParts.size >= 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { - ParsedCommand.Invite(userId) + ParsedCommand.Invite( + userId, + textMessage.substring(Command.INVITE.length + userId.length) + .trim() + .takeIf { it.isNotBlank() } + ) } else { ParsedCommand.ErrorSyntax(Command.INVITE) } @@ -114,12 +137,14 @@ object CommandParser { Command.KICK_USER.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] - if (MatrixPatterns.isUserId(userId)) { - val reason = textMessage.substring(Command.KICK_USER.command.length - + 1 - + userId.length).trim() - ParsedCommand.KickUser(userId, reason) + if (MatrixPatterns.isUserId(userId)) { + ParsedCommand.KickUser( + userId, + textMessage.substring(Command.KICK_USER.length + userId.length) + .trim() + .takeIf { it.isNotBlank() } + ) } else { ParsedCommand.ErrorSyntax(Command.KICK_USER) } @@ -130,12 +155,14 @@ object CommandParser { Command.BAN_USER.command -> { if (messageParts.size >= 2) { val userId = messageParts[1] - if (MatrixPatterns.isUserId(userId)) { - val reason = textMessage.substring(Command.BAN_USER.command.length - + 1 - + userId.length).trim() - ParsedCommand.BanUser(userId, reason) + if (MatrixPatterns.isUserId(userId)) { + ParsedCommand.BanUser( + userId, + textMessage.substring(Command.BAN_USER.length + userId.length) + .trim() + .takeIf { it.isNotBlank() } + ) } else { ParsedCommand.ErrorSyntax(Command.BAN_USER) } @@ -144,11 +171,16 @@ object CommandParser { } } Command.UNBAN_USER.command -> { - if (messageParts.size == 2) { + if (messageParts.size >= 2) { val userId = messageParts[1] if (MatrixPatterns.isUserId(userId)) { - ParsedCommand.UnbanUser(userId) + ParsedCommand.UnbanUser( + userId, + textMessage.substring(Command.UNBAN_USER.length + userId.length) + .trim() + .takeIf { it.isNotBlank() } + ) } else { ParsedCommand.ErrorSyntax(Command.UNBAN_USER) } diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt index 89438c8a9d..dd7c0c7e86 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt @@ -34,14 +34,14 @@ sealed class ParsedCommand { // Valid commands: class SendEmote(val message: CharSequence) : ParsedCommand() - class BanUser(val userId: String, val reason: String) : ParsedCommand() - class UnbanUser(val userId: String) : ParsedCommand() + class BanUser(val userId: String, val reason: String?) : ParsedCommand() + class UnbanUser(val userId: String, val reason: String?) : ParsedCommand() class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand() - class Invite(val userId: String) : ParsedCommand() - class JoinRoom(val roomAlias: String) : ParsedCommand() - class PartRoom(val roomAlias: String) : ParsedCommand() + class Invite(val userId: String, val reason: String?) : ParsedCommand() + class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand() + class PartRoom(val roomAlias: String, val reason: String?) : ParsedCommand() class ChangeTopic(val topic: String) : ParsedCommand() - class KickUser(val userId: String, val reason: String) : ParsedCommand() + class KickUser(val userId: String, val reason: String?) : ParsedCommand() class ChangeDisplayName(val displayName: String) : ParsedCommand() class SetMarkdown(val enable: Boolean) : ParsedCommand() object ClearScalarToken : ParsedCommand() diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt index 9994ee5002..f0997de372 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt @@ -21,6 +21,8 @@ import androidx.appcompat.app.AlertDialog import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState 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.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity @@ -37,12 +39,16 @@ class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSetti override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - keysBackupSettingsRecyclerView.setController(keysBackupSettingsRecyclerViewController) - + keysBackupSettingsRecyclerView.configureWith(keysBackupSettingsRecyclerViewController) keysBackupSettingsRecyclerViewController.listener = this } + override fun onDestroyView() { + keysBackupSettingsRecyclerViewController.listener = null + keysBackupSettingsRecyclerView.cleanup() + super.onDestroyView() + } + override fun invalidate() = withState(viewModel) { state -> keysBackupSettingsRecyclerViewController.setData(state) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index fe98501e73..ac8d429cb1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -25,7 +25,6 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationMenuView -import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotx.R @@ -46,7 +45,6 @@ private const val INDEX_PEOPLE = 1 private const val INDEX_ROOMS = 2 class HomeDetailFragment @Inject constructor( - private val session: Session, val homeDetailViewModelFactory: HomeDetailViewModel.Factory, private val avatarRenderer: AvatarRenderer ) : VectorBaseFragment(), KeysBackupBanner.Delegate { @@ -56,9 +54,7 @@ class HomeDetailFragment @Inject constructor( private val viewModel: HomeDetailViewModel by fragmentViewModel() private lateinit var sharedActionViewModel: HomeSharedActionViewModel - override fun getLayoutResId(): Int { - return R.layout.fragment_home_detail - } + override fun getLayoutResId() = R.layout.fragment_home_detail override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt index 59f31ec2ee..4230ea030d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomDirectoryUsersFragment.kt @@ -23,9 +23,7 @@ import com.airbnb.mvrx.withState import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R -import im.vector.riotx.core.extensions.hideKeyboard -import im.vector.riotx.core.extensions.setupAsSearch -import im.vector.riotx.core.extensions.showKeyboard +import im.vector.riotx.core.extensions.* import im.vector.riotx.core.platform.VectorBaseFragment import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* import javax.inject.Inject @@ -48,10 +46,15 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor( setupCloseView() } + override fun onDestroyView() { + recyclerView.cleanup() + directRoomController.callback = null + super.onDestroyView() + } + private fun setupRecyclerView() { - recyclerView.setHasFixedSize(true) directRoomController.callback = this - recyclerView.setController(directRoomController) + recyclerView.configureWith(directRoomController) } private fun setupSearchByMatrixIdView() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt index 12019fa39e..8108e9705c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomKnownUsersFragment.kt @@ -31,9 +31,7 @@ import com.google.android.material.chip.ChipGroup import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.session.user.model.User import im.vector.riotx.R -import im.vector.riotx.core.extensions.hideKeyboard -import im.vector.riotx.core.extensions.observeEvent -import im.vector.riotx.core.extensions.setupAsSearch +import im.vector.riotx.core.extensions.* import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.utils.DimensionConverter import kotlinx.android.synthetic.main.fragment_create_direct_room.* @@ -67,6 +65,12 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor( } } + override fun onDestroyView() { + knownUsersController.callback = null + recyclerView.cleanup() + super.onDestroyView() + } + override fun onPrepareOptionsMenu(menu: Menu) { withState(viewModel) { val createMenuItem = menu.findItem(R.id.action_create_direct_room) @@ -94,11 +98,10 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor( } private fun setupRecyclerView() { - recyclerView.setHasFixedSize(true) // Don't activate animation as we might have way to much item animation when filtering recyclerView.itemAnimator = null knownUsersController.callback = this - recyclerView.setController(knownUsersController) + recyclerView.configureWith(knownUsersController) } private fun setupFilterView() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt index 39f8c17f05..254571f8cf 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListFragment.kt @@ -23,11 +23,13 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.fragmentViewModel import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.features.home.HomeSharedActionViewModel import im.vector.riotx.features.home.HomeActivitySharedAction +import im.vector.riotx.features.home.HomeSharedActionViewModel import kotlinx.android.synthetic.main.fragment_group_list.* import javax.inject.Inject @@ -45,14 +47,20 @@ class GroupListFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java) groupController.callback = this - stateView.contentView = groupListEpoxyRecyclerView - groupListEpoxyRecyclerView.setController(groupController) + stateView.contentView = groupListView + groupListView.configureWith(groupController) viewModel.subscribe { renderState(it) } viewModel.openGroupLiveData.observeEvent(this) { sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup) } } + override fun onDestroyView() { + groupController.callback = null + groupListView.cleanup() + super.onDestroyView() + } + private fun renderState(state: GroupListViewState) { when (state.asyncGroups) { is Incomplete -> stateView.state = StateView.State.Loading diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsAnimator.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsAnimator.kt new file mode 100644 index 0000000000..2e849dfe38 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsAnimator.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.home.room.breadcrumbs + +import androidx.recyclerview.widget.DefaultItemAnimator + +private const val ANIM_DURATION_IN_MILLIS = 200L + +class BreadcrumbsAnimator : DefaultItemAnimator() { + + init { + addDuration = ANIM_DURATION_IN_MILLIS + removeDuration = ANIM_DURATION_IN_MILLIS + moveDuration = ANIM_DURATION_IN_MILLIS + changeDuration = ANIM_DURATION_IN_MILLIS + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt new file mode 100644 index 0000000000..3e400b37ea --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.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.features.home.room.breadcrumbs + +import android.view.View +import com.airbnb.epoxy.EpoxyController +import im.vector.riotx.core.utils.DebouncedClickListener +import im.vector.riotx.features.home.AvatarRenderer +import javax.inject.Inject + +class BreadcrumbsController @Inject constructor( + private val avatarRenderer: AvatarRenderer +) : EpoxyController() { + + var listener: Listener? = null + + private var viewState: BreadcrumbsViewState? = null + + init { + // We are requesting a model build directly as the first build of epoxy is on the main thread. + // It avoids to build the whole list of breadcrumbs on the main thread. + requestModelBuild() + } + + fun update(viewState: BreadcrumbsViewState) { + this.viewState = viewState + requestModelBuild() + } + + override fun buildModels() { + val safeViewState = viewState ?: return + + // An empty breadcrumbs list can only be temporary because when entering in a room, + // this one is added to the breadcrumbs + + safeViewState.asyncBreadcrumbs.invoke() + ?.forEach { + breadcrumbsItem { + id(it.roomId) + avatarRenderer(avatarRenderer) + roomId(it.roomId) + roomName(it.displayName) + avatarUrl(it.avatarUrl) + unreadNotificationCount(it.notificationCount) + showHighlighted(it.highlightCount > 0) + hasUnreadMessage(it.hasUnreadMessages) + hasDraft(it.userDrafts.isNotEmpty()) + itemClickListener( + DebouncedClickListener(View.OnClickListener { _ -> + listener?.onBreadcrumbClicked(it.roomId) + }) + ) + } + } + } + + interface Listener { + fun onBreadcrumbClicked(roomId: String) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt new file mode 100644 index 0000000000..b8e2cf7987 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt @@ -0,0 +1,68 @@ +/* + * 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.home.room.breadcrumbs + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.fragmentViewModel +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.room.detail.RoomDetailSharedAction +import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel +import kotlinx.android.synthetic.main.fragment_breadcrumbs.* +import javax.inject.Inject + +class BreadcrumbsFragment @Inject constructor( + private val breadcrumbsController: BreadcrumbsController, + val breadcrumbsViewModelFactory: BreadcrumbsViewModel.Factory +) : VectorBaseFragment(), BreadcrumbsController.Listener { + + private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel + private val breadcrumbsViewModel: BreadcrumbsViewModel by fragmentViewModel() + + override fun getLayoutResId() = R.layout.fragment_breadcrumbs + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView() + sharedActionViewModel = activityViewModelProvider.get(RoomDetailSharedActionViewModel::class.java) + + breadcrumbsViewModel.subscribe { renderState(it) } + } + + override fun onDestroyView() { + breadcrumbsRecyclerView.cleanup() + super.onDestroyView() + } + + private fun setupRecyclerView() { + breadcrumbsRecyclerView.configureWith(breadcrumbsController, BreadcrumbsAnimator(), hasFixedSize = false) + breadcrumbsController.listener = this + } + + private fun renderState(state: BreadcrumbsViewState) { + breadcrumbsController.update(state) + } + + // BreadcrumbsController.Listener ************************************************************** + + override fun onBreadcrumbClicked(roomId: String) { + sharedActionViewModel.post(RoomDetailSharedAction.SwitchToRoom(roomId)) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt new file mode 100644 index 0000000000..074c35af00 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt @@ -0,0 +1,60 @@ +/* + * 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.home.room.breadcrumbs + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +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.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView + +@EpoxyModelClass(layout = R.layout.item_breadcrumbs) +abstract class BreadcrumbsItem : VectorEpoxyModel() { + + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute lateinit var roomId: String + @EpoxyAttribute lateinit var roomName: CharSequence + @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute var unreadNotificationCount: Int = 0 + @EpoxyAttribute var showHighlighted: Boolean = false + @EpoxyAttribute var hasUnreadMessage: Boolean = false + @EpoxyAttribute var hasDraft: Boolean = false + @EpoxyAttribute var itemClickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.rootView.setOnClickListener(itemClickListener) + holder.unreadIndentIndicator.isVisible = hasUnreadMessage + avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView) + holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted)) + holder.draftIndentIndicator.isVisible = hasDraft + } + + class Holder : VectorEpoxyHolder() { + val unreadCounterBadgeView by bind(R.id.breadcrumbsUnreadCounterBadgeView) + val unreadIndentIndicator by bind(R.id.breadcrumbsUnreadIndicator) + val draftIndentIndicator by bind(R.id.breadcrumbsDraftBadge) + val avatarImageView by bind(R.id.breadcrumbsImageView) + val rootView by bind(R.id.breadcrumbsRoot) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt new file mode 100644 index 0000000000..83e9e0fb3f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewModel.kt @@ -0,0 +1,66 @@ +/* + * 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.home.room.breadcrumbs + +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.session.Session +import im.vector.matrix.rx.rx +import im.vector.riotx.core.platform.EmptyAction +import im.vector.riotx.core.platform.VectorViewModel +import io.reactivex.schedulers.Schedulers + +class BreadcrumbsViewModel @AssistedInject constructor(@Assisted initialState: BreadcrumbsViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: BreadcrumbsViewState): BreadcrumbsViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: BreadcrumbsViewState): BreadcrumbsViewModel? { + val fragment: BreadcrumbsFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.breadcrumbsViewModelFactory.create(state) + } + } + + init { + observeBreadcrumbs() + } + + override fun handle(action: EmptyAction) { + // No op + } + + // PRIVATE METHODS ***************************************************************************** + + private fun observeBreadcrumbs() { + session.rx() + .liveBreadcrumbs() + .observeOn(Schedulers.computation()) + .execute { asyncBreadcrumbs -> + copy(asyncBreadcrumbs = asyncBreadcrumbs) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewState.kt new file mode 100644 index 0000000000..7cc634c8b0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsViewState.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.home.room.breadcrumbs + +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 BreadcrumbsViewState( + val asyncBreadcrumbs: Async> = Uninitialized +) : MvRxState diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt index eb8118a0c9..431c9e6395 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt @@ -20,17 +20,25 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.appcompat.widget.Toolbar +import androidx.core.view.GravityCompat +import androidx.drawerlayout.widget.DrawerLayout import im.vector.riotx.R +import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.home.room.breadcrumbs.BreadcrumbsFragment +import kotlinx.android.synthetic.main.activity_room_detail.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { - override fun getLayoutRes(): Int { - return R.layout.activity_room_detail - } + override fun getLayoutRes() = R.layout.activity_room_detail + + private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel + + // Simple filter + private var currentRoomId: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -38,14 +46,57 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { if (isFirstCreation()) { val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) ?: return + currentRoomId = roomDetailArgs.roomId replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs) + replaceFragment(R.id.roomDetailDrawerContainer, BreadcrumbsFragment::class.java) } + + sharedActionViewModel = viewModelProvider.get(RoomDetailSharedActionViewModel::class.java) + + sharedActionViewModel + .observe() + .subscribe { sharedAction -> + when (sharedAction) { + is RoomDetailSharedAction.SwitchToRoom -> switchToRoom(sharedAction) + } + } + .disposeOnDestroy() + + drawerLayout.addDrawerListener(drawerListener) + } + + private fun switchToRoom(switchToRoom: RoomDetailSharedAction.SwitchToRoom) { + drawerLayout.closeDrawer(GravityCompat.START) + // Do not replace the Fragment if it's the same roomId + if (currentRoomId != switchToRoom.roomId) { + currentRoomId = switchToRoom.roomId + replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, RoomDetailArgs(switchToRoom.roomId)) + } + } + + override fun onDestroy() { + drawerLayout.removeDrawerListener(drawerListener) + super.onDestroy() } override fun configure(toolbar: Toolbar) { configureToolbar(toolbar) } + private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { + override fun onDrawerStateChanged(newState: Int) { + hideKeyboard() + } + } + + override fun onBackPressed() { + if (drawerLayout.isDrawerOpen(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START) + } else { + super.onBackPressed() + } + } + companion object { private const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS" 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 d50b0c9f68..80f54a9c1f 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 @@ -46,6 +46,7 @@ import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyVisibilityTracker +import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.mvrx.* import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader @@ -69,10 +70,7 @@ import im.vector.riotx.R import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.error.ErrorFormatter -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.extensions.* import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.platform.VectorBaseFragment @@ -193,6 +191,8 @@ class RoomDetailFragment @Inject constructor( private lateinit var sharedActionViewModel: MessageSharedActionViewModel private lateinit var layoutManager: LinearLayoutManager + private var modelBuildListener: OnModelBuildFinishedListener? = null + private lateinit var attachmentsHelper: AttachmentsHelper private lateinit var keyboardStateUtils: KeyboardStateUtils @@ -286,13 +286,16 @@ class RoomDetailFragment @Inject constructor( } override fun onDestroyView() { + timelineEventController.callback = null + timelineEventController.removeModelBuildListener(modelBuildListener) + modelBuildListener = null + debouncer.cancelAll() + recyclerView.cleanup() super.onDestroyView() - recyclerView.adapter = null } override fun onDestroy() { roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) - debouncer.cancelAll() super.onDestroy() } @@ -447,11 +450,7 @@ class RoomDetailFragment @Inject constructor( if (!hasBeenHandled && resultCode == RESULT_OK && data != null) { when (requestCode) { REACTION_SELECT_REQUEST_CODE -> { - val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return - val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return - // TODO check if already reacted with that? + val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction)) } } @@ -470,13 +469,14 @@ class RoomDetailFragment @Inject constructor( recyclerView.layoutManager = layoutManager recyclerView.itemAnimator = null recyclerView.setHasFixedSize(true) - timelineEventController.addModelBuildListener { + modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) updateJumpToReadMarkerViewVisibility() updateJumpToBottomViewVisibility() } + timelineEventController.addModelBuildListener(modelBuildListener) recyclerView.adapter = timelineEventController.adapter recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { @@ -521,27 +521,29 @@ class RoomDetailFragment @Inject constructor( } } - private fun updateJumpToReadMarkerViewVisibility() = jumpToReadMarkerView.post { - withState(roomDetailViewModel) { - val showJumpToUnreadBanner = when (it.unreadState) { - UnreadState.Unknown, - UnreadState.HasNoUnread -> false - is UnreadState.ReadMarkerNotLoaded -> true - is UnreadState.HasUnread -> { - if (it.canShowJumpToReadMarker) { - val lastVisibleItem = layoutManager.findLastVisibleItemPosition() - val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() - if (positionOfReadMarker == null) { - false + private fun updateJumpToReadMarkerViewVisibility() { + jumpToReadMarkerView?.post { + withState(roomDetailViewModel) { + val showJumpToUnreadBanner = when (it.unreadState) { + UnreadState.Unknown, + UnreadState.HasNoUnread -> false + is UnreadState.ReadMarkerNotLoaded -> true + is UnreadState.HasUnread -> { + if (it.canShowJumpToReadMarker) { + val lastVisibleItem = layoutManager.findLastVisibleItemPosition() + val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() + if (positionOfReadMarker == null) { + false + } else { + positionOfReadMarker > lastVisibleItem + } } else { - positionOfReadMarker > lastVisibleItem + false } - } else { - false } } + jumpToReadMarkerView.isVisible = showJumpToUnreadBanner } - jumpToReadMarkerView.isVisible = showJumpToUnreadBanner } } @@ -1181,7 +1183,7 @@ class RoomDetailFragment @Inject constructor( && userId == session.myUserId) { // Empty composer, current user: start an emote composerLayout.composerEditText.setText(Command.EMOTE.command + " ") - composerLayout.composerEditText.setSelection(Command.EMOTE.command.length + 1) + composerLayout.composerEditText.setSelection(Command.EMOTE.length) } else { val roomMember = roomDetailViewModel.getMember(userId) // TODO move logic outside of fragment diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailSharedAction.kt new file mode 100644 index 0000000000..95dd34ebb8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailSharedAction.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.home.room.detail + +import im.vector.riotx.core.platform.VectorSharedAction + +/** + * Supported navigation actions for [RoomDetailActivity] + */ +sealed class RoomDetailSharedAction : VectorSharedAction { + data class SwitchToRoom(val roomId: String) : RoomDetailSharedAction() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailSharedActionViewModel.kt new file mode 100644 index 0000000000..6f2162bebc --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailSharedActionViewModel.kt @@ -0,0 +1,24 @@ +/* + * 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.home.room.detail + +import im.vector.riotx.core.platform.VectorSharedActionViewModel +import javax.inject.Inject + +/** + * Activity shared view model + */ +class RoomDetailSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() 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 642bce3319..e7a18753cd 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 @@ -143,6 +143,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro timeline.addListener(this) timeline.start() setState { copy(timeline = this@RoomDetailViewModel.timeline) } + + // Inform the SDK that the room is displayed + session.onRoomDisplayed(initialState.roomId) } override fun handle(action: RoomDetailAction) { @@ -197,9 +200,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro invisibleEventsObservable.accept(action) } - fun getMember(userId: String) : RoomMember? { - return room.getRoomMember(userId) + fun getMember(userId: String): RoomMember? { + return room.getRoomMember(userId) } + /** * Convert a send mode to a draft and save the draft */ @@ -263,7 +267,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } session.rx() - .joinRoom(roomId, viaServer) + .joinRoom(roomId, viaServers = viaServer) .map { roomId } .execute { copy(tombstoneEventHandling = it) @@ -484,7 +488,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) - room.invite(invite.userId, object : MatrixCallback { + room.invite(invite.userId, invite.reason, object : MatrixCallback { override fun onSuccess(data: Unit) { _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk) } @@ -550,7 +554,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } private fun handleRejectInvite() { - room.leave(object : MatrixCallback {}) + room.leave(null, object : MatrixCallback {}) } private fun handleAcceptInvite() { @@ -859,7 +863,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro override fun onCleared() { timeline.dispose() - timeline.removeAllListeners() + timeline.removeListener(this) super.onCleared() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt index f220570e69..4827c825cb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsBottomSheet.kt @@ -21,7 +21,6 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import butterknife.ButterKnife @@ -29,6 +28,8 @@ import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.args import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import kotlinx.android.parcel.Parcelize @@ -52,8 +53,8 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { private val displayReadReceiptArgs: DisplayReadReceiptArgs by args() - override fun injectWith(screenComponent: ScreenComponent) { - screenComponent.inject(this) + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -64,12 +65,16 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - recyclerView.adapter = epoxyController.adapter + recyclerView.configureWith(epoxyController, hasFixedSize = false) bottomSheetTitle.text = getString(R.string.seen_by) epoxyController.setData(displayReadReceiptArgs.readReceipts) } + override fun onDestroyView() { + recyclerView.cleanup() + super.onDestroyView() + } + // we are not using state for this one as it's static, so no need to override invalidate() companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 326e19c431..576b9fa0ba 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -25,7 +25,6 @@ import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.VisibilityState -import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -44,7 +43,7 @@ import org.threeten.bp.LocalDateTime import javax.inject.Inject class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, - private val session: Session, + private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val timelineItemFactory: TimelineItemFactory, private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val mergedHeaderItemFactory: MergedHeaderItemFactory, @@ -209,6 +208,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec timelineMediaSizeProvider.recyclerView = recyclerView } + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + timelineMediaSizeProvider.recyclerView = null + contentUploadStateTrackerBinder.clear() + timeline?.removeListener(this) + super.onDetachedFromRecyclerView(recyclerView) + } + override fun buildModels() { val timestamp = System.currentTimeMillis() showingForwardLoader = LoadingItem_() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsAnimator.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsAnimator.kt new file mode 100644 index 0000000000..fbe32e0e2b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsAnimator.kt @@ -0,0 +1,34 @@ +/* + * 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.home.room.detail.timeline.action + +import androidx.recyclerview.widget.DefaultItemAnimator + +private const val ANIM_DURATION_IN_MILLIS = 300L + +/** + * We only want to animate the expand of the "Report content" submenu + */ +class MessageActionsAnimator : DefaultItemAnimator() { + + init { + addDuration = ANIM_DURATION_IN_MILLIS + removeDuration = 0 + moveDuration = 0 + changeDuration = 0 + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index a5bf6f8558..ba772344e0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -19,7 +19,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import butterknife.ButterKnife @@ -27,6 +26,8 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import javax.inject.Inject @@ -48,8 +49,8 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message private lateinit var sharedActionViewModel: MessageSharedActionViewModel - override fun injectWith(screenComponent: ScreenComponent) { - screenComponent.inject(this) + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -61,13 +62,17 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) - recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) - recyclerView.adapter = messageActionsEpoxyController.adapter + recyclerView.configureWith(messageActionsEpoxyController, hasFixedSize = false) // Disable item animation recyclerView.itemAnimator = null messageActionsEpoxyController.listener = this } + override fun onDestroyView() { + recyclerView.cleanup() + super.onDestroyView() + } + override fun onUrlClicked(url: String): Boolean { sharedActionViewModel.post(EventSharedAction.OnUrlClicked(url)) // Always consume @@ -83,6 +88,10 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message override fun didSelectMenuAction(eventAction: EventSharedAction) { if (eventAction is EventSharedAction.ReportContent) { // Toggle report menu + // Enable item animation + if (recyclerView.itemAnimator == null) { + recyclerView.itemAnimator = MessageActionsAnimator() + } viewModel.handle(MessageActionsAction.ToggleReportMenu) } else { sharedActionViewModel.post(eventAction) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt index 709bcb53c7..8aa7c8561c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryBottomSheet.kt @@ -19,9 +19,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.LinearLayout -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import butterknife.ButterKnife @@ -30,6 +27,8 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData @@ -54,8 +53,8 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() { ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer) } - override fun injectWith(screenComponent: ScreenComponent) { - screenComponent.inject(this) + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -66,13 +65,18 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - recyclerView.adapter = epoxyController.adapter - recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayout.VERTICAL) - recyclerView.addItemDecoration(dividerItemDecoration) + recyclerView.configureWith( + epoxyController, + showDivider = true, + hasFixedSize = false) bottomSheetTitle.text = context?.getString(R.string.message_edits) } + override fun onDestroyView() { + recyclerView.cleanup() + super.onDestroyView() + } + override fun invalidate() = withState(viewModel) { epoxyController.setData(it) super.invalidate() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt index 4661d8f8cd..1a5c6db270 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/edithistory/ViewEditHistoryEpoxyController.kt @@ -28,17 +28,16 @@ import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply import im.vector.riotx.R +import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericItem import im.vector.riotx.core.ui.list.genericItemHeader import im.vector.riotx.core.ui.list.genericLoaderItem -import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.features.html.EventHtmlRenderer import me.gujun.android.span.span import name.fraser.neil.plaintext.diff_match_patch -import timber.log.Timber -import java.util.Calendar +import java.util.* /** * Epoxy controller for edit history list @@ -104,9 +103,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, ?: nContent.first val dmp = diff_match_patch() val diff = dmp.diff_main(nextBody.toString(), body.toString()) - Timber.e("#### Diff: $diff") dmp.diff_cleanupSemantic(diff) - Timber.e("#### Diff: $diff") spannedDiff = span { diff.map { when (it.operation) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index a3910664a2..75100e6c03 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -19,14 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.format 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.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.RoomJoinRules -import im.vector.matrix.android.api.session.room.model.RoomJoinRulesContent -import im.vector.matrix.android.api.session.room.model.RoomMember -import im.vector.matrix.android.api.session.room.model.RoomNameContent -import im.vector.matrix.android.api.session.room.model.RoomTopicContent +import im.vector.matrix.android.api.session.room.model.* import im.vector.matrix.android.api.session.room.model.call.CallInviteContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotx.R @@ -36,7 +29,7 @@ import timber.log.Timber import javax.inject.Inject class NoticeEventFormatter @Inject constructor(private val sessionHolder: ActiveSessionHolder, - private val stringProvider: StringProvider) { + private val sp: StringProvider) { fun format(timelineEvent: TimelineEvent): CharSequence? { return when (val type = timelineEvent.root.getClearType()) { @@ -84,36 +77,35 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? { val content = event.getClearContent().toModel() ?: return null return if (content.name.isNullOrBlank()) { - stringProvider.getString(R.string.notice_room_name_removed, senderName) + sp.getString(R.string.notice_room_name_removed, senderName) } else { - stringProvider.getString(R.string.notice_room_name_changed, senderName, content.name) + sp.getString(R.string.notice_room_name_changed, senderName, content.name) } } private fun formatRoomTombstoneEvent(senderName: String?): CharSequence? { - return stringProvider.getString(R.string.notice_room_update, senderName) + return sp.getString(R.string.notice_room_update, senderName) } private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? { val content = event.getClearContent().toModel() ?: return null return if (content.topic.isNullOrEmpty()) { - stringProvider.getString(R.string.notice_room_topic_removed, senderName) + sp.getString(R.string.notice_room_topic_removed, senderName) } else { - stringProvider.getString(R.string.notice_room_topic_changed, senderName, content.topic) + sp.getString(R.string.notice_room_topic_changed, senderName, content.topic) } } private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? { - val historyVisibility = event.getClearContent().toModel()?.historyVisibility - ?: return null + val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null val formattedVisibility = when (historyVisibility) { - RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) - RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited) - RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) - RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) + RoomHistoryVisibility.SHARED -> sp.getString(R.string.notice_room_visibility_shared) + RoomHistoryVisibility.INVITED -> sp.getString(R.string.notice_room_visibility_invited) + RoomHistoryVisibility.JOINED -> sp.getString(R.string.notice_room_visibility_joined) + RoomHistoryVisibility.WORLD_READABLE -> sp.getString(R.string.notice_room_visibility_world_readable) } - return stringProvider.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility) + return sp.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility) } private fun formatCallEvent(event: Event, senderName: String?): CharSequence? { @@ -122,13 +114,13 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active val content = event.getClearContent().toModel() ?: return null val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO return if (isVideoCall) { - stringProvider.getString(R.string.notice_placed_video_call, senderName) + sp.getString(R.string.notice_placed_video_call, senderName) } else { - stringProvider.getString(R.string.notice_placed_voice_call, senderName) + sp.getString(R.string.notice_placed_voice_call, senderName) } } - EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, senderName) - EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, senderName) + EventType.CALL_ANSWER == event.type -> sp.getString(R.string.notice_answered_call, senderName) + EventType.CALL_HANGUP == event.type -> sp.getString(R.string.notice_ended_call, senderName) else -> null } } @@ -150,12 +142,11 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active if (eventContent?.displayName != prevEventContent?.displayName) { val displayNameText = when { prevEventContent?.displayName.isNullOrEmpty() -> - stringProvider.getString(R.string.notice_display_name_set, event.senderId, eventContent?.displayName) + sp.getString(R.string.notice_display_name_set, event.senderId, eventContent?.displayName) eventContent?.displayName.isNullOrEmpty() -> - stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName) + sp.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName) else -> - stringProvider.getString(R.string.notice_display_name_changed_from, - event.senderId, prevEventContent?.displayName, eventContent?.displayName) + sp.getString(R.string.notice_display_name_changed_from, event.senderId, prevEventContent?.displayName, eventContent?.displayName) } displayText.append(displayNameText) } @@ -163,73 +154,96 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active if (eventContent?.avatarUrl != prevEventContent?.avatarUrl) { val displayAvatarText = if (displayText.isNotEmpty()) { displayText.append(" ") - stringProvider.getString(R.string.notice_avatar_changed_too) + sp.getString(R.string.notice_avatar_changed_too) } else { - stringProvider.getString(R.string.notice_avatar_url_changed, senderName) + sp.getString(R.string.notice_avatar_url_changed, senderName) } displayText.append(displayAvatarText) } if (displayText.isEmpty()) { displayText.append( - stringProvider.getString(R.string.notice_member_no_changes, senderName) + sp.getString(R.string.notice_member_no_changes, senderName) ) } return displayText.toString() } private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? { - val senderDisplayName = senderName ?: event.senderId - val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: "" - return when { - Membership.INVITE == eventContent?.membership -> { + val senderDisplayName = senderName ?: event.senderId ?: "" + val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: "" + return when (eventContent?.membership) { + Membership.INVITE -> { val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId when { eventContent.thirdPartyInvite != null -> { - val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid - ?: event.stateKey - stringProvider.getString(R.string.notice_room_third_party_registered_invite, - userWhoHasAccepted, eventContent.thirdPartyInvite?.displayName) + val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey + val threePidDisplayName = eventContent.thirdPartyInvite?.displayName ?: "" + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_third_party_registered_invite_with_reason, userWhoHasAccepted, threePidDisplayName, reason) + } ?: sp.getString(R.string.notice_room_third_party_registered_invite, userWhoHasAccepted, threePidDisplayName) } event.stateKey == selfUserId -> - stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName) + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_invite_you_with_reason, senderDisplayName, reason) + } ?: sp.getString(R.string.notice_room_invite_you, senderDisplayName) event.stateKey.isNullOrEmpty() -> - stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName) + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_invite_no_invitee_with_reason, senderDisplayName, reason) + } ?: sp.getString(R.string.notice_room_invite_no_invitee, senderDisplayName) else -> - stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName) + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_invite_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName) } } - Membership.JOIN == eventContent?.membership -> - stringProvider.getString(R.string.notice_room_join, senderDisplayName) - Membership.LEAVE == eventContent?.membership -> + Membership.JOIN -> + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_join_with_reason, senderDisplayName, reason) + } ?: sp.getString(R.string.notice_room_join, senderDisplayName) + Membership.LEAVE -> // 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked - return if (event.senderId == event.stateKey) { + if (event.senderId == event.stateKey) { if (prevEventContent?.membership == Membership.INVITE) { - stringProvider.getString(R.string.notice_room_reject, senderDisplayName) + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_reject_with_reason, senderDisplayName, reason) + } ?: sp.getString(R.string.notice_room_reject, senderDisplayName) } else { - stringProvider.getString(R.string.notice_room_leave, senderDisplayName) + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_leave_with_reason, senderDisplayName, reason) + } ?: sp.getString(R.string.notice_room_leave, senderDisplayName) } } else if (prevEventContent?.membership == Membership.INVITE) { - stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName) + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_withdraw_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName) } else if (prevEventContent?.membership == Membership.JOIN) { - stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_kick_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) } else if (prevEventContent?.membership == Membership.BAN) { - stringProvider.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName) + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_unban_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName) } else { null } - Membership.BAN == eventContent?.membership -> - stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName) - Membership.KNOCK == eventContent?.membership -> - stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) - else -> null + Membership.BAN -> + eventContent.safeReason?.let { + sp.getString(R.string.notice_room_ban_with_reason, senderDisplayName, targetDisplayName, it) + } ?: sp.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName) + Membership.KNOCK -> + eventContent.safeReason?.let { reason -> + sp.getString(R.string.notice_room_kick_with_reason, senderDisplayName, targetDisplayName, reason) + } ?: sp.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) + else -> null } } private fun formatJoinRulesEvent(event: Event, senderName: String?): CharSequence? { val content = event.getClearContent().toModel() ?: return null return when (content.joinRules) { - RoomJoinRules.INVITE -> stringProvider.getString(R.string.room_join_rules_invite, senderName) - RoomJoinRules.PUBLIC -> stringProvider.getString(R.string.room_join_rules_public, senderName) + RoomJoinRules.INVITE -> sp.getString(R.string.room_join_rules_invite, senderName) + RoomJoinRules.PUBLIC -> sp.getString(R.string.room_join_rules_public, senderName) else -> null } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index 3fdce63668..d80c625e8f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -25,12 +25,14 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.di.ScreenScope import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.TextUtils import im.vector.riotx.features.ui.getMessageTextColor import javax.inject.Inject +@ScreenScope class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, private val colorProvider: ColorProvider, private val errorFormatter: ErrorFormatter) { @@ -40,7 +42,7 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess fun bind(eventId: String, isLocalFile: Boolean, progressLayout: ViewGroup) { - activeSessionHolder.getActiveSession().also { session -> + activeSessionHolder.getSafeActiveSession()?.also { session -> val uploadStateTracker = session.contentUploadProgressTracker() val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter) updateListeners[eventId] = updateListener @@ -49,13 +51,19 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess } fun unbind(eventId: String) { - activeSessionHolder.getActiveSession().also { session -> + activeSessionHolder.getSafeActiveSession()?.also { session -> val uploadStateTracker = session.contentUploadProgressTracker() updateListeners[eventId]?.also { uploadStateTracker.untrack(eventId, it) } } } + + fun clear() { + activeSessionHolder.getSafeActiveSession()?.also { + it.contentUploadProgressTracker().clear() + } + } } private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineMediaSizeProvider.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineMediaSizeProvider.kt index 0d11cf8a31..fc3ae98f5c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineMediaSizeProvider.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineMediaSizeProvider.kt @@ -19,11 +19,12 @@ package im.vector.riotx.features.home.room.detail.timeline.helper import androidx.recyclerview.widget.RecyclerView import im.vector.riotx.core.di.ScreenScope import javax.inject.Inject +import kotlin.math.roundToInt @ScreenScope class TimelineMediaSizeProvider @Inject constructor() { - lateinit var recyclerView: RecyclerView + var recyclerView: RecyclerView? = null private var cachedSize: Pair? = null fun getMaxSize(): Pair { @@ -31,17 +32,17 @@ class TimelineMediaSizeProvider @Inject constructor() { } private fun computeMaxSize(): Pair { - val width = recyclerView.width - val height = recyclerView.height + val width = recyclerView?.width ?: 0 + val height = recyclerView?.height ?: 0 val maxImageWidth: Int val maxImageHeight: Int // landscape / portrait if (width < height) { - maxImageWidth = Math.round(width * 0.7f) - maxImageHeight = Math.round(height * 0.5f) + maxImageWidth = (width * 0.7f).roundToInt() + maxImageHeight = (height * 0.5f).roundToInt() } else { - maxImageWidth = Math.round(width * 0.5f) - maxImageHeight = Math.round(height * 0.7f) + maxImageWidth = (width * 0.5f).roundToInt() + maxImageHeight = (height * 0.7f).roundToInt() } return Pair(maxImageWidth, maxImageHeight) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt index d5df8f7b40..8fddc4c06a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/reactions/ViewReactionsBottomSheet.kt @@ -20,7 +20,6 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import butterknife.ButterKnife @@ -29,6 +28,8 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData @@ -49,8 +50,8 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() { @Inject lateinit var epoxyController: ViewReactionsEpoxyController - override fun injectWith(screenComponent: ScreenComponent) { - screenComponent.inject(this) + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -61,11 +62,15 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - recyclerView.adapter = epoxyController.adapter + recyclerView.configureWith(epoxyController, hasFixedSize = false) bottomSheetTitle.text = context?.getString(R.string.reactions) } + override fun onDestroyView() { + recyclerView.cleanup() + super.onDestroyView() + } + override fun invalidate() = withState(viewModel) { epoxyController.setData(it) super.invalidate() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/filtered/FilteredRoomsActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/filtered/FilteredRoomsActivity.kt index c4bb0d9b15..04e738cec8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/filtered/FilteredRoomsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/filtered/FilteredRoomsActivity.kt @@ -36,9 +36,7 @@ class FilteredRoomsActivity : VectorBaseActivity() { return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomListFragment } - override fun getLayoutRes(): Int { - return R.layout.activity_filtered_rooms - } + override fun getLayoutRes() = R.layout.activity_filtered_rooms override fun injectWith(injector: ScreenComponent) { injector.inject(this) 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 04d1802264..00d964b28c 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 @@ -26,6 +26,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.mvrx.* import com.google.android.material.snackbar.Snackbar import im.vector.matrix.android.api.failure.Failure @@ -35,13 +36,13 @@ import im.vector.matrix.android.api.session.room.notification.RoomNotificationSt import im.vector.riotx.R import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.cleanup 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.RoomListQuickActionsSharedAction 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 im.vector.riotx.features.home.room.list.widget.FabMenuView import im.vector.riotx.features.notifications.NotificationDrawerManager @@ -65,6 +66,7 @@ class RoomListFragment @Inject constructor( ) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener { + private var modelBuildListener: OnModelBuildFinishedListener? = null private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel private val roomListParams: RoomListParams by args() private val roomListViewModel: RoomListViewModel by fragmentViewModel() @@ -118,8 +120,12 @@ class RoomListFragment @Inject constructor( } override fun onDestroyView() { + roomController.removeModelBuildListener(modelBuildListener) + modelBuildListener = null + roomListView.cleanup() + roomController.listener = null + createChatFabMenu.listener = null super.onDestroyView() - roomListView.adapter = null } private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) { @@ -198,7 +204,8 @@ class RoomListFragment @Inject constructor( roomListView.layoutManager = layoutManager roomListView.itemAnimator = RoomListAnimator() roomController.listener = this - roomController.addModelBuildListener { it.dispatchTo(stateRestorer) } + modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) } + roomController.addModelBuildListener(modelBuildListener) roomListView.adapter = roomController.adapter stateView.contentView = roomListView } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index e5924d9f2a..a9ea831723 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -123,7 +123,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, ) } - session.getRoom(roomId)?.join(emptyList(), object : MatrixCallback { + session.getRoom(roomId)?.join(callback = object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined @@ -158,7 +158,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, ) } - session.getRoom(roomId)?.leave(object : MatrixCallback { + session.getRoom(roomId)?.leave(null, object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data. // Instead, we wait for the room to be rejected @@ -197,7 +197,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, } private fun handleLeaveRoom(action: RoomListAction.LeaveRoom) { - session.getRoom(action.roomId)?.leave(object : MatrixCallback { + session.getRoom(action.roomId)?.leave(null, object : MatrixCallback { override fun onFailure(failure: Throwable) { _viewEvents.post(RoomListViewEvents.Failure(failure)) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt index 4107bf01b2..6ffe37cb15 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt @@ -42,7 +42,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri init { // We are requesting a model build directly as the first build of epoxy is on the main thread. - // It avoids to build the the whole list of rooms on the main thread. + // It avoids to build the whole list of rooms on the main thread. requestModelBuild() } 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 3a85cf26fa..60a26c8151 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 @@ -21,7 +21,6 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import butterknife.ButterKnife @@ -29,6 +28,8 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.features.navigation.Navigator import kotlinx.android.parcel.Parcelize @@ -56,8 +57,8 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R override val showExpanded = true - override fun injectWith(screenComponent: ScreenComponent) { - screenComponent.inject(this) + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -69,13 +70,17 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) - recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) - recyclerView.adapter = roomListActionsEpoxyController.adapter + recyclerView.configureWith(roomListActionsEpoxyController, hasFixedSize = false) // Disable item animation recyclerView.itemAnimator = null roomListActionsEpoxyController.listener = this } + override fun onDestroyView() { + recyclerView.cleanup() + super.onDestroyView() + } + override fun invalidate() = withState(viewModel) { roomListActionsEpoxyController.setData(it) super.invalidate() diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index de76f6b416..00207cbfbf 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -539,7 +539,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi setState { copy( asyncHomeServerLoginFlowRequest = Uninitialized, - homeServerUrl = action.homeServerUrl, + homeServerUrl = data.homeServerUrl, loginMode = loginMode, loginModeSupportedTypes = data.loginFlowResponse.flows.mapNotNull { it.type }.toList() ) diff --git a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt index 08110f3b33..83d68f5f31 100755 --- a/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/terms/LoginTermsFragment.kt @@ -24,6 +24,8 @@ import butterknife.OnClick import com.airbnb.mvrx.args import im.vector.riotx.R import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.features.login.AbstractLoginFragment import im.vector.riotx.features.login.LoginAction @@ -55,8 +57,7 @@ class LoginTermsFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - loginTermsPolicyList.setController(policyController) + loginTermsPolicyList.configureWith(policyController) policyController.listener = this val list = ArrayList() @@ -69,6 +70,12 @@ class LoginTermsFragment @Inject constructor( loginTermsViewState = LoginTermsViewState(list) } + override fun onDestroyView() { + loginTermsPolicyList.cleanup() + policyController.listener = null + super.onDestroyView() + } + private fun renderState() { policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt index 63cd1c5ce6..c9dc131b42 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationBroadcastReceiver.kt @@ -74,14 +74,14 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { private fun handleJoinRoom(roomId: String) { activeSessionHolder.getSafeActiveSession()?.let { session -> session.getRoom(roomId) - ?.join(emptyList(), object : MatrixCallback {}) + ?.join(callback = object : MatrixCallback {}) } } private fun handleRejectRoom(roomId: String) { activeSessionHolder.getSafeActiveSession()?.let { session -> session.getRoom(roomId) - ?.leave(object : MatrixCallback {}) + ?.leave(callback = object : MatrixCallback {}) } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserFragment.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserFragment.kt index 5e705a70c2..1dc9f34924 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserFragment.kt @@ -17,12 +17,18 @@ package im.vector.riotx.features.reactions import android.os.Bundle import android.view.View -import androidx.recyclerview.widget.RecyclerView +import androidx.lifecycle.observe import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.synthetic.main.emoji_chooser_fragment.* import javax.inject.Inject -class EmojiChooserFragment @Inject constructor() : VectorBaseFragment() { +class EmojiChooserFragment @Inject constructor( + private val emojiRecyclerAdapter: EmojiRecyclerAdapter +) : VectorBaseFragment(), + EmojiRecyclerAdapter.InteractionListener, + ReactionClickListener { override fun getLayoutResId() = R.layout.emoji_chooser_fragment @@ -31,10 +37,29 @@ class EmojiChooserFragment @Inject constructor() : VectorBaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java) - viewModel.initWithContext(context!!) - (view as? RecyclerView)?.let { - it.adapter = viewModel.adapter - it.adapter?.notifyDataSetChanged() + + emojiRecyclerAdapter.reactionClickListener = this + emojiRecyclerAdapter.interactionListener = this + + emojiRecyclerView.adapter = emojiRecyclerAdapter + + viewModel.moveToSection.observe(viewLifecycleOwner) { section -> + emojiRecyclerAdapter.scrollToSection(section) } } + + override fun firstVisibleSectionChange(section: Int) { + viewModel.setCurrentSection(section) + } + + override fun onReactionSelected(reaction: String) { + viewModel.onReactionSelected(reaction) + } + + override fun onDestroyView() { + emojiRecyclerView.cleanup() + emojiRecyclerAdapter.reactionClickListener = null + emojiRecyclerAdapter.interactionListener = null + super.onDestroyView() + } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserViewModel.kt index bbde2ac54c..9a0317f454 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiChooserViewModel.kt @@ -15,7 +15,6 @@ */ package im.vector.riotx.features.reactions -import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import im.vector.riotx.core.utils.LiveEvent @@ -23,36 +22,26 @@ import javax.inject.Inject class EmojiChooserViewModel @Inject constructor() : ViewModel() { - var adapter: EmojiRecyclerAdapter? = null - val emojiSourceLiveData: MutableLiveData = MutableLiveData() - val navigateEvent: MutableLiveData> = MutableLiveData() var selectedReaction: String? = null var eventId: String? = null val currentSection: MutableLiveData = MutableLiveData() + val moveToSection: MutableLiveData = MutableLiveData() - var reactionClickListener = object : ReactionClickListener { - override fun onReactionSelected(reaction: String) { - selectedReaction = reaction - navigateEvent.value = LiveEvent(NAVIGATE_FINISH) - } + fun onReactionSelected(reaction: String) { + selectedReaction = reaction + navigateEvent.value = LiveEvent(NAVIGATE_FINISH) } - fun initWithContext(context: Context) { - // TODO load async - val emojiDataSource = EmojiDataSource(context) - emojiSourceLiveData.value = emojiDataSource - adapter = EmojiRecyclerAdapter(emojiDataSource, reactionClickListener) - adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener { - override fun firstVisibleSectionChange(section: Int) { - currentSection.value = section - } - } + // Called by the Fragment, when the List is scrolled + fun setCurrentSection(section: Int) { + currentSection.value = section } - fun scrollToSection(sectionIndex: Int) { - adapter?.scrollToSection(sectionIndex) + // Called by the Activity, when a tab item is clicked + fun scrollToSection(section: Int) { + moveToSection.value = section } companion object { diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiDataSource.kt deleted file mode 100644 index 2853975938..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiDataSource.kt +++ /dev/null @@ -1,91 +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.riotx.features.reactions - -import android.content.Context -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import com.squareup.moshi.Moshi -import im.vector.riotx.R - -class EmojiDataSource(val context: Context) { - - var rawData: EmojiData? = null - - init { - context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input -> - val moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter(EmojiData::class.java) - val inputAsString = input.bufferedReader().use { it.readText() } - this.rawData = jsonAdapter.fromJson(inputAsString) - // this.rawData = mb.fr(InputStreamReader(it), EmojiData::class.java) - } - } - @JsonClass(generateAdapter = true) - data class EmojiData(val categories: List, - val emojis: Map, - val aliases: Map) - - @JsonClass(generateAdapter = true) - data class EmojiCategory(val id: String, val name: String, val emojis: List) - - @JsonClass(generateAdapter = true) - data class EmojiItem( - @Json(name = "a") val name: String, - @Json(name = "b") val unicode: String, - @Json(name = "j") val keywords: List?, - val k: List?) { - - var _emojiText: String? = null - - fun emojiString() : String { - if (_emojiText == null) { - val utf8Text = unicode.split("-").joinToString("") { "\\u$it" } // "\u0048\u0065\u006C\u006C\u006F World" - _emojiText = fromUnicode(utf8Text) - } - return _emojiText!! - } - } - - companion object { - fun fromUnicode(unicode: String): String { - val str = unicode.replace("\\", "") - val arr = str.split("u".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - val text = StringBuffer() - for (i in 1 until arr.size) { - val hexVal = Integer.parseInt(arr[i], 16) - text.append(Character.toChars(hexVal)) - } - return text.toString() - } - } - -// name: 'a', -// unified: 'b', -// non_qualified: 'c', -// has_img_apple: 'd', -// has_img_google: 'e', -// has_img_twitter: 'f', -// has_img_emojione: 'g', -// has_img_facebook: 'h', -// has_img_messenger: 'i', -// keywords: 'j', -// sheet: 'k', -// emoticons: 'l', -// text: 'm', -// short_names: 'n', -// added_in: 'o', -} diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiReactionPickerActivity.kt index 85e4eecf21..562ad6f5b2 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiReactionPickerActivity.kt @@ -35,6 +35,7 @@ import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.reactions.data.EmojiDataSource import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.* import timber.log.Timber @@ -44,7 +45,6 @@ import javax.inject.Inject /** * * TODO: Loading indicator while getting emoji data source? - * TODO: migrate to MvRx * TODO: Finish Refactor to vector base activity */ class EmojiReactionPickerActivity : VectorBaseActivity(), @@ -54,13 +54,15 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), lateinit var viewModel: EmojiChooserViewModel - override fun getMenuRes(): Int = R.menu.menu_emoji_reaction_picker + override fun getMenuRes() = R.menu.menu_emoji_reaction_picker - override fun getLayoutRes(): Int = R.layout.activity_emoji_reaction_picker + override fun getLayoutRes() = R.layout.activity_emoji_reaction_picker - override fun getTitleRes(): Int = R.string.title_activity_emoji_reaction_picker + override fun getTitleRes() = R.string.title_activity_emoji_reaction_picker + @Inject lateinit var emojiSearchResultViewModelFactory: EmojiSearchResultViewModel.Factory @Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider + @Inject lateinit var emojiDataSource: EmojiDataSource private val searchResultViewModel: EmojiSearchResultViewModel by viewModel() @@ -93,22 +95,18 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), viewModel.eventId = intent.getStringExtra(EXTRA_EVENT_ID) - viewModel.emojiSourceLiveData.observe(this, Observer { - it.rawData?.categories?.let { categories -> - for (category in categories) { - val s = category.emojis[0] - tabLayout.newTab() - .also { tab -> - tab.text = it.rawData!!.emojis[s]!!.emojiString() - tab.contentDescription = category.name - } - .also { tab -> - tabLayout.addTab(tab) - } - } - tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener) - } - }) + emojiDataSource.rawData.categories.forEach { category -> + val s = category.emojis[0] + tabLayout.newTab() + .also { tab -> + tab.text = emojiDataSource.rawData.emojis[s]!!.emoji + tab.contentDescription = category.name + } + .also { tab -> + tabLayout.addTab(tab) + } + } + tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener) viewModel.currentSection.observe(this, Observer { section -> section?.let { @@ -136,7 +134,6 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), override fun compatibilityFontUpdate(typeface: Typeface?) { EmojiDrawView.configureTextPaint(this, typeface) - searchResultViewModel.dataSource } override fun onDestroy() { @@ -206,13 +203,19 @@ class EmojiReactionPickerActivity : VectorBaseActivity(), companion object { - const val EXTRA_EVENT_ID = "EXTRA_EVENT_ID" - const val EXTRA_REACTION_RESULT = "EXTRA_REACTION_RESULT" + private const val EXTRA_EVENT_ID = "EXTRA_EVENT_ID" + private const val EXTRA_REACTION_RESULT = "EXTRA_REACTION_RESULT" fun intent(context: Context, eventId: String): Intent { val intent = Intent(context, EmojiReactionPickerActivity::class.java) intent.putExtra(EXTRA_EVENT_ID, eventId) return intent } + + fun getOutput(data: Intent): Pair? { + val eventId = data.getStringExtra(EXTRA_EVENT_ID) ?: return null + val reaction = data.getStringExtra(EXTRA_REACTION_RESULT) ?: return null + return eventId to reaction + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiRecyclerAdapter.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiRecyclerAdapter.kt index b5270bad29..efccb9c917 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiRecyclerAdapter.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiRecyclerAdapter.kt @@ -30,22 +30,25 @@ import androidx.recyclerview.widget.RecyclerView import androidx.transition.AutoTransition import androidx.transition.TransitionManager import im.vector.riotx.R +import im.vector.riotx.features.reactions.data.EmojiDataSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import javax.inject.Inject import kotlin.math.abs /** * * TODO: Configure Span using available width and emoji size - * TODO: Search * TODO: Performances * TODO: Scroll to section - Find a way to snap section to the top */ -class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null, - private var reactionClickListener: ReactionClickListener?) : +class EmojiRecyclerAdapter @Inject constructor( + private val dataSource: EmojiDataSource +) : RecyclerView.Adapter() { + var reactionClickListener: ReactionClickListener? = null var interactionListener: InteractionListener? = null private var mRecyclerView: RecyclerView? = null @@ -66,13 +69,12 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null, private val itemClickListener = View.OnClickListener { view -> mRecyclerView?.getChildLayoutPosition(view)?.let { itemPosition -> if (itemPosition != RecyclerView.NO_POSITION) { - val categories = dataSource?.rawData?.categories ?: return@OnClickListener val sectionNumber = getSectionForAbsoluteIndex(itemPosition) if (!isSection(itemPosition)) { - val sectionMojis = categories[sectionNumber].emojis + val sectionMojis = dataSource.rawData.categories[sectionNumber].emojis val sectionOffset = getSectionOffset(sectionNumber) val emoji = sectionMojis[itemPosition - sectionOffset] - val item = dataSource.rawData!!.emojis.getValue(emoji).emojiString() + val item = dataSource.rawData.emojis.getValue(emoji).emoji reactionClickListener?.onReactionSelected(item) } } @@ -113,7 +115,7 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null, } fun scrollToSection(section: Int) { - if (section < 0 || section >= dataSource?.rawData?.categories?.size ?: 0) { + if (section < 0 || section >= dataSource.rawData.categories.size) { // ignore return } @@ -145,14 +147,12 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null, } private fun isSection(position: Int): Boolean { - dataSource?.rawData?.categories?.let { categories -> - var sectionOffset = 1 - var lastItemInSection: Int - for (category in categories) { - lastItemInSection = sectionOffset + category.emojis.size - 1 - if (position == sectionOffset - 1) return true - sectionOffset = lastItemInSection + 2 - } + var sectionOffset = 1 + var lastItemInSection: Int + dataSource.rawData.categories.forEach { category -> + lastItemInSection = sectionOffset + category.emojis.size - 1 + if (position == sectionOffset - 1) return true + sectionOffset = lastItemInSection + 2 } return false } @@ -161,13 +161,11 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null, var sectionOffset = 1 var lastItemInSection: Int var index = 0 - dataSource?.rawData?.categories?.let { - for (category in it) { - lastItemInSection = sectionOffset + category.emojis.size - 1 - if (position <= lastItemInSection) return index - sectionOffset = lastItemInSection + 2 - index++ - } + dataSource.rawData.categories.forEach { category -> + lastItemInSection = sectionOffset + category.emojis.size - 1 + if (position <= lastItemInSection) return index + sectionOffset = lastItemInSection + 2 + index++ } return index } @@ -176,36 +174,32 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null, // Todo cache this for fast access var sectionOffset = 1 var lastItemInSection: Int - dataSource?.rawData?.categories?.let { - for ((index, category) in it.withIndex()) { - lastItemInSection = sectionOffset + category.emojis.size - 1 - if (section == index) return sectionOffset - sectionOffset = lastItemInSection + 2 - } + dataSource.rawData.categories.forEachIndexed { index, category -> + lastItemInSection = sectionOffset + category.emojis.size - 1 + if (section == index) return sectionOffset + sectionOffset = lastItemInSection + 2 } return sectionOffset } override fun onBindViewHolder(holder: ViewHolder, position: Int) { beginTraceSession("MyAdapter.onBindViewHolder") - dataSource?.rawData?.categories?.let { categories -> - val sectionNumber = getSectionForAbsoluteIndex(position) - if (isSection(position)) { - holder.bind(categories[sectionNumber].name) - } else { - val sectionMojis = categories[sectionNumber].emojis - val sectionOffset = getSectionOffset(sectionNumber) - val emoji = sectionMojis[position - sectionOffset] - val item = dataSource.rawData!!.emojis[emoji]!!.emojiString() - (holder as EmojiViewHolder).data = item - if (scrollState != ScrollState.SETTLING || !isFastScroll) { + val sectionNumber = getSectionForAbsoluteIndex(position) + if (isSection(position)) { + holder.bind(dataSource.rawData.categories[sectionNumber].name) + } else { + val sectionMojis = dataSource.rawData.categories[sectionNumber].emojis + val sectionOffset = getSectionOffset(sectionNumber) + val emoji = sectionMojis[position - sectionOffset] + val item = dataSource.rawData.emojis[emoji]!!.emoji + (holder as EmojiViewHolder).data = item + if (scrollState != ScrollState.SETTLING || !isFastScroll) { // Log.i("PERF","Bind with draw at position:$position") - holder.bind(item) - } else { + holder.bind(item) + } else { // Log.i("PERF","Bind without draw at position:$position") - toUpdateWhenNotBusy.add(item to holder) - holder.bind(null) - } + toUpdateWhenNotBusy.add(item to holder) + holder.bind(null) } } endTraceSession() @@ -226,15 +220,8 @@ class EmojiRecyclerAdapter(private val dataSource: EmojiDataSource? = null, super.onViewRecycled(holder) } - override fun getItemCount(): Int { - return dataSource?.rawData?.categories?.let { - var count = /*number of sections*/ it.size - for (ad in it) { - count += ad.emojis.size - } - count - } ?: 0 - } + override fun getItemCount() = dataSource.rawData.categories + .sumBy { emojiCategory -> 1 /* Section */ + emojiCategory.emojis.size } abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { abstract fun bind(s: String?) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultController.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultController.kt index 3e8f1c9769..208d9d7a56 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultController.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultController.kt @@ -24,9 +24,10 @@ import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.ui.list.genericFooterItem import javax.inject.Inject -class EmojiSearchResultController @Inject constructor(val stringProvider: StringProvider, - private val fontProvider: EmojiCompatFontProvider) - : TypedEpoxyController() { +class EmojiSearchResultController @Inject constructor( + private val stringProvider: StringProvider, + private val fontProvider: EmojiCompatFontProvider +) : TypedEpoxyController() { var emojiTypeface: Typeface? = fontProvider.typeface diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultFragment.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultFragment.kt index e5b46c2176..a62b2d39cb 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultFragment.kt @@ -17,43 +17,42 @@ package im.vector.riotx.features.reactions import android.os.Bundle import android.view.View -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState 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.core.utils.LiveEvent -import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.* +import kotlinx.android.synthetic.main.fragment_generic_recycler.* import javax.inject.Inject class EmojiSearchResultFragment @Inject constructor( private val epoxyController: EmojiSearchResultController -) : VectorBaseFragment() { +) : VectorBaseFragment(), ReactionClickListener { - override fun getLayoutResId(): Int = R.layout.fragment_generic_recycler_epoxy + override fun getLayoutResId() = R.layout.fragment_generic_recycler - val viewModel: EmojiSearchResultViewModel by activityViewModel() + private val viewModel: EmojiSearchResultViewModel by activityViewModel() - var sharedViewModel: EmojiChooserViewModel? = null + private lateinit var sharedViewModel: EmojiChooserViewModel override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedViewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java) + epoxyController.listener = this + recyclerView.configureWith(epoxyController, showDivider = true) + } - epoxyController.listener = object : ReactionClickListener { - override fun onReactionSelected(reaction: String) { - sharedViewModel?.selectedReaction = reaction - sharedViewModel?.navigateEvent?.value = LiveEvent(EmojiChooserViewModel.NAVIGATE_FINISH) - } - } + override fun onDestroyView() { + epoxyController.listener = null + recyclerView.cleanup() + super.onDestroyView() + } - val lmgr = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - epoxyRecyclerView.layoutManager = lmgr - val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, lmgr.orientation) - epoxyRecyclerView.addItemDecoration(dividerItemDecoration) - epoxyRecyclerView.setController(epoxyController) + override fun onReactionSelected(reaction: String) { + sharedViewModel.selectedReaction = reaction + sharedViewModel.navigateEvent.value = LiveEvent(EmojiChooserViewModel.NAVIGATE_FINISH) } override fun invalidate() = withState(viewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultItem.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultItem.kt index 1b117035d9..55bf29e25f 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultItem.kt @@ -22,12 +22,14 @@ import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.reactions.data.EmojiItem @EpoxyModelClass(layout = R.layout.item_emoji_result) abstract class EmojiSearchResultItem : EpoxyModelWithHolder() { @EpoxyAttribute - lateinit var emojiItem: EmojiDataSource.EmojiItem + lateinit var emojiItem: EmojiItem @EpoxyAttribute var currentQuery: String? = null @@ -41,12 +43,12 @@ abstract class EmojiSearchResultItem : EpoxyModelWithHolder = emptyList() + val results: List = emptyList() ) : MvRxState -class EmojiSearchResultViewModel(val dataSource: EmojiDataSource, initialState: EmojiSearchResultViewState) +class EmojiSearchResultViewModel @AssistedInject constructor( + @Assisted initialState: EmojiSearchResultViewState, + private val dataSource: EmojiDataSource) : VectorViewModel(initialState) { + @AssistedInject.Factory + interface Factory { + fun create(initialState: EmojiSearchResultViewState): EmojiSearchResultViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: EmojiSearchResultViewState): EmojiSearchResultViewModel? { + val activity: EmojiReactionPickerActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.emojiSearchResultViewModelFactory.create(state) + } + } + override fun handle(action: EmojiSearchAction) { when (action) { is EmojiSearchAction.UpdateQuery -> updateQuery(action) @@ -35,26 +55,27 @@ class EmojiSearchResultViewModel(val dataSource: EmojiDataSource, initialState: } private fun updateQuery(action: EmojiSearchAction.UpdateQuery) { + val words = action.queryString.split("\\s".toRegex()) setState { copy( query = action.queryString, - results = dataSource.rawData?.emojis?.toList() - ?.map { it.second } - ?.filter { - it.name.contains(action.queryString, true) - || action.queryString.split("\\s".toRegex()).fold(true, { prev, q -> - prev && (it.keywords?.any { it.contains(q, true) } ?: false) + // First add emojis with name matching query, sorted by name + // Then emojis with keyword matching any of the word in the query, sorted by name + results = dataSource.rawData.emojis + .values + .filter { emojiItem -> + emojiItem.name.contains(action.queryString, true) + } + .sortedBy { it.name } + + dataSource.rawData.emojis + .values + .filter { emojiItem -> + words.fold(true, { prev, word -> + prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) } }) - } ?: emptyList() + } + .sortedBy { it.name } ) } } - - companion object : MvRxViewModelFactory { - - override fun create(viewModelContext: ViewModelContext, state: EmojiSearchResultViewState): EmojiSearchResultViewModel? { - // TODO get the data source from activity? share it with other fragment - return EmojiSearchResultViewModel(EmojiDataSource(viewModelContext.activity), state) - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiCategory.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiCategory.kt new file mode 100644 index 0000000000..a6c238a4c4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiCategory.kt @@ -0,0 +1,27 @@ +/* + * 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.reactions.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EmojiCategory( + @Json(name = "id") val id: String, + @Json(name = "name") val name: String, + @Json(name = "emojis") val emojis: List +) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiData.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiData.kt new file mode 100644 index 0000000000..f2094bfc7b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiData.kt @@ -0,0 +1,27 @@ +/* + * 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.reactions.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EmojiData( + @Json(name = "categories") val categories: List, + @Json(name = "emojis") val emojis: Map, + @Json(name = "aliases") val aliases: Map +) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt new file mode 100644 index 0000000000..a326828112 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -0,0 +1,36 @@ +/* + * 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.reactions.data + +import android.content.res.Resources +import com.squareup.moshi.Moshi +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenScope +import javax.inject.Inject + +@ScreenScope +class EmojiDataSource @Inject constructor( + resources: Resources +) { + val rawData = resources.openRawResource(R.raw.emoji_picker_datasource) + .use { input -> + Moshi.Builder() + .build() + .adapter(EmojiData::class.java) + .fromJson(input.bufferedReader().use { it.readText() }) + } + ?: EmojiData(emptyList(), emptyMap(), emptyMap()) +} diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiItem.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiItem.kt new file mode 100644 index 0000000000..57083e8467 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiItem.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.features.reactions.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * name: 'a', + * unified: 'b', + * non_qualified: 'c', + * has_img_apple: 'd', + * has_img_google: 'e', + * has_img_twitter: 'f', + * has_img_emojione: 'g', + * has_img_facebook: 'h', + * has_img_messenger: 'i', + * keywords: 'j', + * sheet: 'k', + * emoticons: 'l', + * text: 'm', + * short_names: 'n', + * added_in: 'o' + */ +@JsonClass(generateAdapter = true) +data class EmojiItem( + @Json(name = "a") val name: String, + @Json(name = "b") val unicode: String, + @Json(name = "j") val keywords: List = emptyList() +) { + // Cannot be private... + var cache: String? = null + + val emoji: String + get() { + cache?.let { return it } + + // "\u0048\u0065\u006C\u006C\u006F World" + val utf8Text = unicode + .split("-") + .joinToString("") { "\\u$it" } + return fromUnicode(utf8Text) + .also { cache = it } + } + + companion object { + private fun fromUnicode(unicode: String): String { + val arr = unicode + .replace("\\", "") + .split("u".toRegex()) + .dropLastWhile { it.isEmpty() } + return buildString { + for (i in 1 until arr.size) { + val hexVal = Integer.parseInt(arr[i], 16) + append(Character.toChars(hexVal)) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt index b41c563256..1d8ed48b08 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/PublicRoomsFragment.kt @@ -19,7 +19,6 @@ package im.vector.riotx.features.roomdirectory import android.os.Bundle import android.view.MenuItem import android.view.View -import androidx.recyclerview.widget.LinearLayoutManager import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState @@ -28,6 +27,8 @@ import com.jakewharton.rxbinding3.appcompat.queryTextChanges import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.riotx.R import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseFragment import io.reactivex.rxkotlin.subscribeBy @@ -62,6 +63,9 @@ class PublicRoomsFragment @Inject constructor( it.setDisplayHomeAsUpEnabled(true) } + sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) + setupRecyclerView() + publicRoomsFilter.queryTextChanges() .debounce(500, TimeUnit.MILLISECONDS) .subscribeBy { @@ -79,6 +83,12 @@ class PublicRoomsFragment @Inject constructor( } } + override fun onDestroyView() { + publicRoomsController.callback = null + publicRoomsList.cleanup() + super.onDestroyView() + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_room_directory_change_protocol -> { @@ -90,22 +100,11 @@ class PublicRoomsFragment @Inject constructor( } } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) - setupRecyclerView() - } - private fun setupRecyclerView() { val epoxyVisibilityTracker = EpoxyVisibilityTracker() epoxyVisibilityTracker.attach(publicRoomsList) - - val layoutManager = LinearLayoutManager(context) - - publicRoomsList.layoutManager = layoutManager + publicRoomsList.configureWith(publicRoomsController) publicRoomsController.callback = this - - publicRoomsList.setController(publicRoomsController) } override fun onPublicRoomClicked(publicRoom: PublicRoom, joinState: JoinState) { diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt index 685e1aa282..d89f0e2b99 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/RoomDirectoryViewModel.kt @@ -214,7 +214,7 @@ class RoomDirectoryViewModel @AssistedInject constructor(@Assisted initialState: ) } - session.joinRoom(action.roomId, emptyList(), object : MatrixCallback { + session.joinRoom(action.roomId, callback = object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt index 0dec14f50e..aacc21916a 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -19,11 +19,12 @@ package im.vector.riotx.features.roomdirectory.createroom import android.os.Bundle import android.view.MenuItem import android.view.View -import androidx.recyclerview.widget.LinearLayoutManager import com.airbnb.mvrx.Success import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState 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.roomdirectory.RoomDirectorySharedAction import im.vector.riotx.features.roomdirectory.RoomDirectorySharedActionViewModel @@ -50,6 +51,12 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C } } + override fun onDestroyView() { + createRoomForm.cleanup() + createRoomController.listener = null + super.onDestroyView() + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_create_room -> { @@ -62,12 +69,8 @@ class CreateRoomFragment @Inject constructor(private val createRoomController: C } private fun setupRecyclerView() { - val layoutManager = LinearLayoutManager(context) - - createRoomForm.layoutManager = layoutManager + createRoomForm.configureWith(createRoomController) createRoomController.listener = this - - createRoomForm.setController(createRoomController) } override fun onNameChange(newName: String) { diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt index ce7a57deba..1299919d2b 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/picker/RoomDirectoryPickerFragment.kt @@ -19,12 +19,13 @@ package im.vector.riotx.features.roomdirectory.picker import android.os.Bundle import android.view.MenuItem import android.view.View -import androidx.recyclerview.widget.LinearLayoutManager import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.matrix.android.api.session.room.model.thirdparty.RoomDirectoryData 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.roomdirectory.RoomDirectoryAction import im.vector.riotx.features.roomdirectory.RoomDirectorySharedAction @@ -60,6 +61,12 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie setupRecyclerView() } + override fun onDestroyView() { + roomDirectoryPickerList.cleanup() + roomDirectoryPickerController.callback = null + super.onDestroyView() + } + override fun getMenuRes() = R.menu.menu_directory_server_picker override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -73,12 +80,8 @@ class RoomDirectoryPickerFragment @Inject constructor(val roomDirectoryPickerVie } private fun setupRecyclerView() { - val layoutManager = LinearLayoutManager(context) - - roomDirectoryPickerList.layoutManager = layoutManager + roomDirectoryPickerList.configureWith(roomDirectoryPickerController) roomDirectoryPickerController.callback = this - - roomDirectoryPickerList.setController(roomDirectoryPickerController) } override fun onRoomDirectoryClicked(roomDirectoryData: RoomDirectoryData) { diff --git a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt index 9ffb64556f..54c86537d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/roomdirectory/roompreview/RoomPreviewViewModel.kt @@ -97,7 +97,7 @@ class RoomPreviewViewModel @AssistedInject constructor(@Assisted initialState: R ) } - session.joinRoom(state.roomId, emptyList(), object : MatrixCallback { + session.joinRoom(state.roomId, callback = object : MatrixCallback { override fun onSuccess(data: Unit) { // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // Instead, we wait for the room to be joined diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt index 6f43114eb4..3f69b5880e 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsNotificationsTroubleshootFragment.kt @@ -29,6 +29,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager import butterknife.BindView import im.vector.riotx.R +import im.vector.riotx.core.extensions.cleanup import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.features.rageshake.BugReporter @@ -136,6 +137,11 @@ class VectorSettingsNotificationsTroubleshootFragment @Inject constructor( testManager?.runDiagnostic() } + override fun onDestroyView() { + mRecyclerView.cleanup() + super.onDestroyView() + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (resultCode == Activity.RESULT_OK && requestCode == NotificationTroubleshootTestManager.REQ_CODE_FIX) { testManager?.retry() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt index 11e473ae24..a6b8a5414f 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/ignored/VectorSettingsIgnoredUsersFragment.kt @@ -26,10 +26,12 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.cleanup +import im.vector.riotx.core.extensions.configureWith import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseFragment -import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.* +import kotlinx.android.synthetic.main.fragment_generic_recycler.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import javax.inject.Inject @@ -39,7 +41,7 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor( private val errorFormatter: ErrorFormatter ) : VectorBaseFragment(), IgnoredUsersController.Callback { - override fun getLayoutResId() = R.layout.fragment_generic_recycler_epoxy + override fun getLayoutResId() = R.layout.fragment_generic_recycler private val ignoredUsersViewModel: IgnoredUsersViewModel by fragmentViewModel() @@ -49,12 +51,18 @@ class VectorSettingsIgnoredUsersFragment @Inject constructor( waiting_view_status_text.setText(R.string.please_wait) waiting_view_status_text.isVisible = true ignoredUsersController.callback = this - epoxyRecyclerView.setController(ignoredUsersController) + recyclerView.configureWith(ignoredUsersController) ignoredUsersViewModel.requestErrorLiveData.observeEvent(this) { displayErrorDialog(it) } } + override fun onDestroyView() { + ignoredUsersController.callback = null + recyclerView.cleanup() + super.onDestroyView() + } + override fun onResume() { super.onResume() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/push/PushGateWayController.kt b/vector/src/main/java/im/vector/riotx/features/settings/push/PushGateWayController.kt new file mode 100644 index 0000000000..d8d23fbaf4 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/push/PushGateWayController.kt @@ -0,0 +1,51 @@ +/* + * 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.settings.push + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.riotx.R +import im.vector.riotx.core.resources.StringProvider +import im.vector.riotx.core.ui.list.genericFooterItem +import javax.inject.Inject + +class PushGateWayController @Inject constructor( + private val stringProvider: StringProvider +) : TypedEpoxyController() { + + override fun buildModels(data: PushGatewayViewState?) { + data?.pushGateways?.invoke()?.let { pushers -> + if (pushers.isEmpty()) { + genericFooterItem { + id("footer") + text(stringProvider.getString(R.string.settings_push_gateway_no_pushers)) + } + } else { + pushers.forEach { + pushGatewayItem { + id("${it.pushKey}_${it.appId}") + pusher(it) + } + } + } + } ?: run { + genericFooterItem { + id("loading") + text(stringProvider.getString(R.string.loading)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysFragment.kt index ea23ba2583..d5c19e8781 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/push/PushGatewaysFragment.kt @@ -17,71 +17,43 @@ package im.vector.riotx.features.settings.push import android.os.Bundle -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.airbnb.epoxy.TypedEpoxyController +import android.view.View import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState 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.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseFragment -import im.vector.riotx.core.resources.StringProvider -import im.vector.riotx.core.ui.list.genericFooterItem -import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.* +import kotlinx.android.synthetic.main.fragment_generic_recycler.* import javax.inject.Inject // Referenced in vector_settings_notifications.xml class PushGatewaysFragment @Inject constructor( - val pushGatewaysViewModelFactory: PushGatewaysViewModel.Factory + val pushGatewaysViewModelFactory: PushGatewaysViewModel.Factory, + private val epoxyController: PushGateWayController ) : VectorBaseFragment() { - override fun getLayoutResId(): Int = R.layout.fragment_generic_recycler_epoxy + override fun getLayoutResId() = R.layout.fragment_generic_recycler private val viewModel: PushGatewaysViewModel by fragmentViewModel(PushGatewaysViewModel::class) - private val epoxyController by lazy { PushGateWayController(StringProvider(requireContext().resources)) } override fun onResume() { super.onResume() (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_notifications_targets) } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - val lmgr = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - epoxyRecyclerView.layoutManager = lmgr - val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, - lmgr.orientation) - epoxyRecyclerView.addItemDecoration(dividerItemDecoration) - epoxyRecyclerView.setController(epoxyController) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + recyclerView.configureWith(epoxyController, showDivider = true) + } + + override fun onDestroyView() { + recyclerView.cleanup() + super.onDestroyView() } override fun invalidate() = withState(viewModel) { state -> epoxyController.setData(state) } - - class PushGateWayController(private val stringProvider: StringProvider) : TypedEpoxyController() { - override fun buildModels(data: PushGatewayViewState?) { - data?.pushGateways?.invoke()?.let { pushers -> - if (pushers.isEmpty()) { - genericFooterItem { - id("footer") - text(stringProvider.getString(R.string.settings_push_gateway_no_pushers)) - } - } else { - pushers.forEach { - pushGatewayItem { - id("${it.pushKey}_${it.appId}") - pusher(it) - } - } - } - } ?: run { - genericFooterItem { - id("footer") - text(stringProvider.getString(R.string.loading)) - } - } - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesFragment.kt index 5e14053d0b..bee9cf54d9 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesFragment.kt @@ -16,23 +16,23 @@ package im.vector.riotx.features.settings.push import android.os.Bundle -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView +import android.view.View import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState 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.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.ui.list.genericFooterItem -import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.* +import kotlinx.android.synthetic.main.fragment_generic_recycler.* // Referenced in vector_settings_notifications.xml class PushRulesFragment : VectorBaseFragment() { - override fun getLayoutResId(): Int = R.layout.fragment_generic_recycler_epoxy + override fun getLayoutResId() = R.layout.fragment_generic_recycler private val viewModel: PushRulesViewModel by fragmentViewModel(PushRulesViewModel::class) @@ -43,14 +43,14 @@ class PushRulesFragment : VectorBaseFragment() { (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_push_rules) } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - val lmgr = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - epoxyRecyclerView.layoutManager = lmgr - val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context, - lmgr.orientation) - epoxyRecyclerView.addItemDecoration(dividerItemDecoration) - epoxyRecyclerView.setController(epoxyController) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + recyclerView.configureWith(epoxyController, showDivider = true) + } + + override fun onDestroyView() { + recyclerView.cleanup() + super.onDestroyView() } override fun invalidate() = withState(viewModel) { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt index 33b0744d4f..e48c8246d2 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt @@ -50,9 +50,7 @@ class IncomingShareActivity : return supportFragmentManager.findFragmentById(R.id.shareRoomListFragmentContainer) as? RoomListFragment } - override fun getLayoutRes(): Int { - return R.layout.activity_incoming_share - } + override fun getLayoutRes() = R.layout.activity_incoming_share override fun injectWith(injector: ScreenComponent) { injector.inject(this) diff --git a/vector/src/main/res/layout/activity_home.xml b/vector/src/main/res/layout/activity_home.xml index 0ce124b787..61fb1b5ad4 100644 --- a/vector/src/main/res/layout/activity_home.xml +++ b/vector/src/main/res/layout/activity_home.xml @@ -17,6 +17,7 @@ android:layout_height="match_parent" /> + diff --git a/vector/src/main/res/layout/activity_room_detail.xml b/vector/src/main/res/layout/activity_room_detail.xml index 4d8fc23f24..cc10341d2f 100644 --- a/vector/src/main/res/layout/activity_room_detail.xml +++ b/vector/src/main/res/layout/activity_room_detail.xml @@ -1,14 +1,36 @@ - - + android:layout_height="match_parent" + tools:openDrawer="start"> + + + + + + + + + + + + + + android:id="@+id/roomDetailDrawerContainer" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="start" /> - - - \ No newline at end of file + diff --git a/vector/src/main/res/layout/emoji_chooser_fragment.xml b/vector/src/main/res/layout/emoji_chooser_fragment.xml index 197e7ce6ef..586411f3d5 100644 --- a/vector/src/main/res/layout/emoji_chooser_fragment.xml +++ b/vector/src/main/res/layout/emoji_chooser_fragment.xml @@ -1,7 +1,7 @@ - - \ No newline at end of file + tools:spanCount="10" /> diff --git a/vector/src/main/res/layout/fragment_breadcrumbs.xml b/vector/src/main/res/layout/fragment_breadcrumbs.xml new file mode 100644 index 0000000000..5cdd2e964a --- /dev/null +++ b/vector/src/main/res/layout/fragment_breadcrumbs.xml @@ -0,0 +1,8 @@ + + diff --git a/vector/src/main/res/layout/fragment_create_direct_room.xml b/vector/src/main/res/layout/fragment_create_direct_room.xml index f8450d1e6e..5ad2da1032 100644 --- a/vector/src/main/res/layout/fragment_create_direct_room.xml +++ b/vector/src/main/res/layout/fragment_create_direct_room.xml @@ -122,7 +122,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/createDirectRoomFilterDivider" /> - - - - - - - - - + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_emoji_result.xml b/vector/src/main/res/layout/item_emoji_result.xml index cc93ee0d5d..9ef4b36bc8 100644 --- a/vector/src/main/res/layout/item_emoji_result.xml +++ b/vector/src/main/res/layout/item_emoji_result.xml @@ -3,49 +3,50 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="?riotx_background" + android:foreground="?attr/selectableItemBackground" + android:gravity="center_vertical" + android:minHeight="48dp" android:orientation="horizontal" - android:paddingEnd="8dp" - android:paddingStart="8dp" - android:minHeight="44dp"> + android:paddingStart="@dimen/layout_horizontal_margin" + android:paddingEnd="@dimen/layout_horizontal_margin"> + android:layout_weight="1" + android:orientation="vertical"> - + android:textColor="?riotx_text_secondary" + android:textSize="14sp" + android:visibility="gone" + tools:text="Smile, foo, bar" + tools:visibility="visible" /> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 57e9ca35d6..2e4d04354b 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -284,6 +284,7 @@ Unable to register : email ownership failure Please enter a valid URL This URL is not reachable, please check it + This is not a valid Matrix server address Cannot reach a homeserver at this URL, please check it Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect Mobile