Merge branch 'release/0.10.0'

This commit is contained in:
Benoit Marty 2019-12-10 15:47:36 +01:00
commit 902a9aa243
137 changed files with 2083 additions and 674 deletions

View file

@ -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) Changes in RiotX 0.9.1 (2019-12-05)
=================================================== ===================================================

View file

@ -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 ```json
{ {

View file

@ -57,8 +57,9 @@ class RxRoom(private val room: Room) {
room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it) room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it)
} }
fun joinRoom(viaServers: List<String> = emptyList()): Single<Unit> = Single.create { fun joinRoom(reason: String? = null,
room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it) viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
room.join(reason, viaServers, MatrixCallbackSingle(it)).toSingle(it)
} }
fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> { fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {

View file

@ -38,6 +38,10 @@ class RxSession(private val session: Session) {
return session.liveGroupSummaries().asObservable() return session.liveGroupSummaries().asObservable()
} }
fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
return session.liveBreadcrumbs().asObservable()
}
fun liveSyncState(): Observable<SyncState> { fun liveSyncState(): Observable<SyncState> {
return session.syncState().asObservable() return session.syncState().asObservable()
} }
@ -72,8 +76,10 @@ class RxSession(private val session: Session) {
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it) session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it)
} }
fun joinRoom(roomId: String, viaServers: List<String> = emptyList()): Single<Unit> = Single.create { fun joinRoom(roomId: String,
session.joinRoom(roomId, viaServers, MatrixCallbackSingle(it)).toSingle(it) reason: String? = null,
viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
session.joinRoom(roomId, reason, viaServers, MatrixCallbackSingle(it)).toSingle(it)
} }
} }

View file

@ -22,7 +22,8 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
sealed class LoginFlowResult { sealed class LoginFlowResult {
data class Success( data class Success(
val loginFlowResponse: LoginFlowResponse, val loginFlowResponse: LoginFlowResponse,
val isLoginAndRegistrationSupported: Boolean val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String
) : LoginFlowResult() ) : LoginFlowResult()
object OutdatedHomeserver : LoginFlowResult() object OutdatedHomeserver : LoginFlowResult()

View file

@ -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())) data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false"))) object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
// When server send an error, but it cannot be interpreted as a MatrixError // 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())) data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString()))

View file

@ -22,6 +22,8 @@ interface ContentUploadStateTracker {
fun untrack(key: String, updateListener: UpdateListener) fun untrack(key: String, updateListener: UpdateListener)
fun clear()
interface UpdateListener { interface UpdateListener {
fun onUpdate(state: State) fun onUpdate(state: State)
} }

View file

@ -30,12 +30,16 @@ interface RoomDirectoryService {
/** /**
* Get rooms from directory * Get rooms from directory
*/ */
fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback<PublicRoomsResponse>): Cancelable fun getPublicRooms(server: String?,
publicRoomsParams: PublicRoomsParams,
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
/** /**
* Join a room by id * Join a room by id
*/ */
fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable fun joinRoom(roomId: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/** /**
* Fetches the overall metadata about protocols supported by the homeserver. * Fetches the overall metadata about protocols supported by the homeserver.

View file

@ -30,14 +30,17 @@ interface RoomService {
/** /**
* Create a room asynchronously * Create a room asynchronously
*/ */
fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable fun createRoom(createRoomParams: CreateRoomParams,
callback: MatrixCallback<String>): Cancelable
/** /**
* Join a room by id * Join a room by id
* @param roomId the roomId of the room to join * @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. * @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, fun joinRoom(roomId: String,
reason: String? = null,
viaServers: List<String> = emptyList(), viaServers: List<String> = emptyList(),
callback: MatrixCallback<Unit>): Cancelable callback: MatrixCallback<Unit>): Cancelable
@ -54,8 +57,21 @@ interface RoomService {
*/ */
fun liveRoomSummaries(): LiveData<List<RoomSummary>> fun liveRoomSummaries(): LiveData<List<RoomSummary>>
/**
* Get a live list of Breadcrumbs
* @return the [LiveData] of [RoomSummary]
*/
fun liveBreadcrumbs(): LiveData<List<RoomSummary>>
/**
* 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 * Mark all rooms as read
*/ */
fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable fun markAllAsRead(roomIds: List<String>,
callback: MatrixCallback<Unit>): Cancelable
} }

View file

@ -52,16 +52,21 @@ interface MembershipService {
/** /**
* Invite a user in the room * Invite a user in the room
*/ */
fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable fun invite(userId: String,
reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
/** /**
* Join the room, or accept an invitation. * Join the room, or accept an invitation.
*/ */
fun join(viaServers: List<String> = emptyList(), callback: MatrixCallback<Unit>): Cancelable fun join(reason: String? = null,
viaServers: List<String> = emptyList(),
callback: MatrixCallback<Unit>): Cancelable
/** /**
* Leave the room, or reject an invitation. * Leave the room, or reject an invitation.
*/ */
fun leave(callback: MatrixCallback<Unit>): Cancelable fun leave(reason: String? = null,
callback: MatrixCallback<Unit>): Cancelable
} }

View file

@ -26,9 +26,13 @@ import im.vector.matrix.android.api.session.events.model.UnsignedData
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class RoomMember( data class RoomMember(
@Json(name = "membership") val membership: Membership, @Json(name = "membership") val membership: Membership,
@Json(name = "reason") val reason: String? = null,
@Json(name = "displayname") val displayName: String? = null, @Json(name = "displayname") val displayName: String? = null,
@Json(name = "avatar_url") val avatarUrl: String? = null, @Json(name = "avatar_url") val avatarUrl: String? = null,
@Json(name = "is_direct") val isDirect: Boolean = false, @Json(name = "is_direct") val isDirect: Boolean = false,
@Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null, @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null,
@Json(name = "unsigned") val unsignedData: UnsignedData? = null @Json(name = "unsigned") val unsignedData: UnsignedData? = null
) ) {
val safeReason
get() = reason?.takeIf { it.isNotBlank() }
}

View file

@ -50,6 +50,7 @@ interface RelationService {
/** /**
* Sends a reaction (emoji) to the targetedEvent. * 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 targetEventId the id of the event being reacted
* @param reaction the reaction (preferably emoji) * @param reaction the reaction (preferably emoji)
*/ */

View file

@ -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.api.auth.data.Versions
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse 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.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.login.ResetPasswordMailConfirmed
import im.vector.matrix.android.internal.auth.registration.* import im.vector.matrix.android.internal.auth.registration.*
import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.NetworkConstants
@ -31,6 +32,12 @@ import retrofit2.http.*
*/ */
internal interface AuthAPI { internal interface AuthAPI {
/**
* Get a Riot config file
*/
@GET("config.json")
fun getRiotConfig(): Call<RiotConfig>
/** /**
* Get the version information of the homeserver * Get the version information of the homeserver
*/ */

View file

@ -16,16 +16,19 @@
package im.vector.matrix.android.internal.auth package im.vector.matrix.android.internal.auth
import android.net.Uri
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.auth.data.* import im.vector.matrix.android.api.auth.data.*
import im.vector.matrix.android.api.auth.login.LoginWizard import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.RegistrationWizard 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.session.Session
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse 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.db.PendingSessionData
import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard
import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard
@ -40,6 +43,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import javax.inject.Inject import javax.inject.Inject
import javax.net.ssl.HttpsURLConnection
internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
private val okHttpClient: Lazy<OkHttpClient>, private val okHttpClient: Lazy<OkHttpClient>,
@ -84,7 +88,12 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
{ {
if (it is LoginFlowResult.Success) { if (it is LoginFlowResult.Success) {
// The homeserver exists and up to date, keep the config // 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) } .also { data -> pendingSessionStore.savePendingSessionData(data) }
} }
callback.onSuccess(it) callback.onSuccess(it)
@ -97,20 +106,71 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
.toCancelable() .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<Versions> {
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) val authAPI = buildAuthAPI(homeServerConnectionConfig)
// First check the homeserver version // Ok, try to get the config.json file of a RiotWeb client
val versions = executeRequest<Versions> { val riotConfig = executeRequest<RiotConfig> {
apiCall = authAPI.versions() 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<Versions> {
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 // Get the login flow
val loginFlowResponse = executeRequest<LoginFlowResponse> { val loginFlowResponse = executeRequest<LoginFlowResponse> {
apiCall = authAPI.getLoginFlows() apiCall = authAPI.getLoginFlows()
} }
LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk()) LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
} else { } else {
// Not supported // Not supported
LoginFlowResult.OutdatedHomeserver LoginFlowResult.OutdatedHomeserver

View file

@ -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?
)

View file

@ -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.CryptoRoomEntity
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
/** /**
* Get or create a room * Get or create a room
*/ */
internal fun CryptoRoomEntity.Companion.getOrCreate(realm: Realm, roomId: String): CryptoRoomEntity { internal fun CryptoRoomEntity.Companion.getOrCreate(realm: Realm, roomId: String): CryptoRoomEntity {
return getById(realm, roomId) return getById(realm, roomId) ?: realm.createObject(roomId)
?: let {
realm.createObject(CryptoRoomEntity::class.java, roomId)
}
} }
/** /**

View file

@ -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.DeviceInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
/** /**
* Get or create a device info * Get or create a device info
*/ */
internal fun DeviceInfoEntity.Companion.getOrCreate(realm: Realm, userId: String, deviceId: String): DeviceInfoEntity { internal fun DeviceInfoEntity.Companion.getOrCreate(realm: Realm, userId: String, deviceId: String): DeviceInfoEntity {
val key = DeviceInfoEntity.createPrimaryKey(userId, deviceId)
return realm.where<DeviceInfoEntity>() return realm.where<DeviceInfoEntity>()
.equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, key)
.findFirst() .findFirst()
?: let { ?: realm.createObject<DeviceInfoEntity>(key)
realm.createObject(DeviceInfoEntity::class.java, DeviceInfoEntity.createPrimaryKey(userId, deviceId)).apply { .apply {
this.deviceId = deviceId this.deviceId = deviceId
} }
}
} }

View file

@ -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.UserEntity
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
/** /**
@ -28,9 +29,7 @@ internal fun UserEntity.Companion.getOrCreate(realm: Realm, userId: String): Use
return realm.where<UserEntity>() return realm.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, userId) .equalTo(UserEntityFields.USER_ID, userId)
.findFirst() .findFirst()
?: let { ?: realm.createObject(userId)
realm.createObject(UserEntity::class.java, userId)
}
} }
/** /**

View file

@ -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<String> = RealmList()
) : RealmObject() {
companion object
}

View file

@ -38,7 +38,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var readMarkerId: String? = null, var readMarkerId: String? = null,
var hasUnreadMessages: Boolean = false, var hasUnreadMessages: Boolean = false,
var tags: RealmList<RoomTagEntity> = RealmList(), var tags: RealmList<RoomTagEntity> = RealmList(),
var userDrafts: UserDraftsEntity? = null var userDrafts: UserDraftsEntity? = null,
var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS
) : RealmObject() { ) : RealmObject() {
private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name
@ -59,5 +60,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
versioningStateStr = value.name versioningStateStr = value.name
} }
companion object companion object {
const val NOT_IN_BREADCRUMBS = -1
}
} }

View file

@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule
SyncEntity::class, SyncEntity::class,
UserEntity::class, UserEntity::class,
IgnoredUserEntity::class, IgnoredUserEntity::class,
BreadcrumbsEntity::class,
EventAnnotationsSummaryEntity::class, EventAnnotationsSummaryEntity::class,
ReactionAggregatedSummaryEntity::class, ReactionAggregatedSummaryEntity::class,
EditAggregatedSummaryEntity::class, EditAggregatedSummaryEntity::class,

View file

@ -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<BreadcrumbsEntity>().findFirst()
}
internal fun BreadcrumbsEntity.Companion.getOrCreate(realm: Realm): BreadcrumbsEntity {
return get(realm) ?: realm.createObject()
}

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ReadMarkerEntity> { internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ReadMarkerEntity> {
@ -28,6 +29,5 @@ internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): Rea
} }
internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity { internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity {
return where(realm, roomId).findFirst() return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
?: realm.createObject(ReadMarkerEntity::class.java, roomId)
} }

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> { internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<ReadReceiptEntity> {
@ -44,10 +45,11 @@ internal fun ReadReceiptEntity.Companion.createUnmanaged(roomId: String, eventId
internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity { internal fun ReadReceiptEntity.Companion.getOrCreate(realm: Realm, roomId: String, userId: String): ReadReceiptEntity {
return ReadReceiptEntity.where(realm, roomId, userId).findFirst() return ReadReceiptEntity.where(realm, roomId, userId).findFirst()
?: realm.createObject(ReadReceiptEntity::class.java, buildPrimaryKey(roomId, userId)).apply { ?: realm.createObject<ReadReceiptEntity>(buildPrimaryKey(roomId, userId))
this.roomId = roomId .apply {
this.userId = userId this.roomId = roomId
} this.userId = userId
}
} }
private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId" private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId"

View file

@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.kotlin.createObject
import io.realm.kotlin.where import io.realm.kotlin.where
internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> { internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> {
@ -32,8 +33,7 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n
} }
internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity { internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity {
return where(realm, roomId).findFirst() return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
?: realm.createObject(RoomSummaryEntity::class.java, roomId)
} }
internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> { internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> {

View file

@ -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.RuntimeJsonAdapterFactory
import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter 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.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.UserAccountDataDirectMessages
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers 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(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES)
.registerSubtype(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST) .registerSubtype(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST)
.registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES) .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) .add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java)
.registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT) .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT)

View file

@ -19,6 +19,7 @@
package im.vector.matrix.android.internal.network package im.vector.matrix.android.internal.network
import com.squareup.moshi.JsonDataException 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.ConsentNotGivenError
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
@ -106,6 +107,9 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
} catch (ex: JsonDataException) { } catch (ex: JsonDataException) {
// This is not a MatrixError // This is not a MatrixError
Timber.w("The error returned by the server 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) return Failure.OtherServerError(errorBodyStr, httpCode)

View file

@ -42,6 +42,10 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
} }
} }
override fun clear() {
listeners.clear()
}
internal fun setFailure(key: String, throwable: Throwable) { internal fun setFailure(key: String, throwable: Throwable) {
val failure = ContentUploadStateTracker.State.Failure(throwable) val failure = ContentUploadStateTracker.State.Failure(throwable)
updateState(key, failure) updateState(key, failure)

View file

@ -44,9 +44,9 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable { override fun joinRoom(roomId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
return joinRoomTask return joinRoomTask
.configureWith(JoinRoomTask.Params(roomId)) { .configureWith(JoinRoomTask.Params(roomId, reason)) {
this.callback = callback this.callback = callback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)

View file

@ -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.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask 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.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.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import io.realm.Realm import io.realm.Realm
@ -43,6 +44,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
private val createRoomTask: CreateRoomTask, private val createRoomTask: CreateRoomTask,
private val joinRoomTask: JoinRoomTask, private val joinRoomTask: JoinRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val markAllRoomsReadTask: MarkAllRoomsReadTask,
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
private val roomFactory: RoomFactory, private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService { 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<String>, callback: MatrixCallback<Unit>): Cancelable { override fun liveBreadcrumbs(): LiveData<List<RoomSummary>> {
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<String>, callback: MatrixCallback<Unit>): Cancelable {
return joinRoomTask return joinRoomTask
.configureWith(JoinRoomTask.Params(roomId, viaServers)) { .configureWith(JoinRoomTask.Params(roomId, reason, viaServers)) {
this.callback = callback this.callback = callback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)

View file

@ -217,7 +217,7 @@ internal interface RoomAPI {
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join")
fun join(@Path("roomId") roomId: String, fun join(@Path("roomId") roomId: String,
@Query("server_name") viaServers: List<String>, @Query("server_name") viaServers: List<String>,
@Body params: Map<String, String>): Call<Unit> @Body params: Map<String, String?>): Call<Unit>
/** /**
* Leave the given room. * Leave the given room.
@ -227,7 +227,7 @@ internal interface RoomAPI {
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave")
fun leave(@Path("roomId") roomId: String, fun leave(@Path("roomId") roomId: String,
@Body params: Map<String, String>): Call<Unit> @Body params: Map<String, String?>): Call<Unit>
/** /**
* Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room. * Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room.

View file

@ -83,8 +83,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
return result return result
} }
override fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable { override fun invite(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
val params = InviteTask.Params(roomId, userId) val params = InviteTask.Params(roomId, userId, reason)
return inviteTask return inviteTask
.configureWith(params) { .configureWith(params) {
this.callback = callback this.callback = callback
@ -92,8 +92,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun join(viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable { override fun join(reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
val params = JoinRoomTask.Params(roomId, viaServers) val params = JoinRoomTask.Params(roomId, reason, viaServers)
return joinTask return joinTask
.configureWith(params) { .configureWith(params) {
this.callback = callback this.callback = callback
@ -101,8 +101,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun leave(callback: MatrixCallback<Unit>): Cancelable { override fun leave(reason: String?, callback: MatrixCallback<Unit>): Cancelable {
val params = LeaveRoomTask.Params(roomId) val params = LeaveRoomTask.Params(roomId, reason)
return leaveRoomTask return leaveRoomTask
.configureWith(params) { .configureWith(params) {
this.callback = callback this.callback = callback

View file

@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class InviteBody( data class InviteBody(
@Json(name = "user_id") val userId: String @Json(name = "user_id") val userId: String,
@Json(name = "reason") val reason: String?
) )

View file

@ -24,7 +24,8 @@ import javax.inject.Inject
internal interface InviteTask : Task<InviteTask.Params, Unit> { internal interface InviteTask : Task<InviteTask.Params, Unit> {
data class Params( data class Params(
val roomId: String, 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) { override suspend fun execute(params: InviteTask.Params) {
return executeRequest { return executeRequest {
val body = InviteBody(params.userId) val body = InviteBody(params.userId, params.reason)
apiCall = roomAPI.invite(params.roomId, body) apiCall = roomAPI.invite(params.roomId, body)
} }
} }

View file

@ -32,6 +32,7 @@ import javax.inject.Inject
internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> { internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> {
data class Params( data class Params(
val roomId: String, val roomId: String,
val reason: String?,
val viaServers: List<String> = emptyList() val viaServers: List<String> = emptyList()
) )
} }
@ -43,7 +44,7 @@ internal class DefaultJoinRoomTask @Inject constructor(private val roomAPI: Room
override suspend fun execute(params: JoinRoomTask.Params) { override suspend fun execute(params: JoinRoomTask.Params) {
executeRequest<Unit> { executeRequest<Unit> {
apiCall = roomAPI.join(params.roomId, params.viaServers, HashMap()) apiCall = roomAPI.join(params.roomId, params.viaServers, mapOf("reason" to params.reason))
} }
val roomId = params.roomId 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) // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)

View file

@ -23,7 +23,8 @@ import javax.inject.Inject
internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> { internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> {
data class Params( 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) { override suspend fun execute(params: LeaveRoomTask.Params) {
return executeRequest { return executeRequest {
apiCall = roomAPI.leave(params.roomId, HashMap()) apiCall = roomAPI.leave(params.roomId, mapOf("reason" to params.reason))
} }
} }
} }

View file

@ -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.model.relation.RelationService
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent 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.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional 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.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity 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.database.query.where
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker 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.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.CancelableWork 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 im.vector.matrix.android.internal.worker.WorkerParamsFactory
import timber.log.Timber import timber.log.Timber
@ -54,6 +58,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask, private val fetchEditHistoryTask: FetchEditHistoryTask,
private val timelineEventMapper: TimelineEventMapper,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor) private val taskExecutor: TaskExecutor)
: RelationService { : RelationService {
@ -64,11 +69,27 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
} }
override fun sendReaction(targetEventId: String, reaction: String): Cancelable { override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) return if (monarchy
.also { saveLocalEcho(it) } .fetchCopyMap(
val sendRelationWork = createSendEventWork(event, true) { realm ->
TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork) TimelineEventEntity.where(realm, roomId, targetEventId).findFirst()
return CancelableWork(context, sendRelationWork.id) },
{ 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 { override fun undoReaction(targetEventId: String, reaction: String): Cancelable {

View file

@ -78,7 +78,7 @@ internal class LocalEchoEventFactory @Inject constructor(
val htmlText = renderer.render(document) val htmlText = renderer.render(document)
if (isFormattedTextPertinent(source, htmlText)) { if (isFormattedTextPertinent(source, htmlText)) {
return TextContent(source, htmlText) return TextContent(text.toString(), htmlText)
} }
} else { } else {
// Try to detect pills // Try to detect pills

View file

@ -289,6 +289,9 @@ internal class DefaultTimeline(
} }
override fun addListener(listener: Timeline.Listener) = synchronized(listeners) { override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
if (listeners.contains(listener)) {
return false
}
listeners.add(listener).also { listeners.add(listener).also {
postSnapshot() postSnapshot()
} }
@ -494,9 +497,9 @@ internal class DefaultTimeline(
return return
} }
val params = PaginationTask.Params(roomId = roomId, val params = PaginationTask.Params(roomId = roomId,
from = token, from = token,
direction = direction.toPaginationDirection(), direction = direction.toPaginationDirection(),
limit = limit) limit = limit)
Timber.v("Should fetch $limit items $direction") Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask cancelableBag += paginationTask
@ -571,7 +574,7 @@ internal class DefaultTimeline(
val timelineEvent = buildTimelineEvent(eventEntity) val timelineEvent = buildTimelineEvent(eventEntity)
if (timelineEvent.isEncrypted() if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) { && timelineEvent.root.mxDecryptionResult == null) {
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) } timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
} }

View file

@ -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.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.accountdata.* 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.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.SaveIgnoredUsersTask
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import im.vector.matrix.android.internal.task.TaskExecutor 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 updateUserAccountDataTask: UpdateUserAccountDataTask,
private val savePushRulesTask: SavePushRulesTask, private val savePushRulesTask: SavePushRulesTask,
private val saveIgnoredUsersTask: SaveIgnoredUsersTask, private val saveIgnoredUsersTask: SaveIgnoredUsersTask,
private val saveBreadcrumbsTask: SaveBreadcrumbsTask,
private val taskExecutor: TaskExecutor) { private val taskExecutor: TaskExecutor) {
suspend fun handle(accountData: UserAccountDataSync?, invites: Map<String, InvitedRoomSync>?) { suspend fun handle(accountData: UserAccountDataSync?, invites: Map<String, InvitedRoomSync>?) {
@ -52,6 +54,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
is UserAccountDataDirectMessages -> handleDirectChatRooms(it) is UserAccountDataDirectMessages -> handleDirectChatRooms(it)
is UserAccountDataPushRules -> handlePushRules(it) is UserAccountDataPushRules -> handlePushRules(it)
is UserAccountDataIgnoredUsers -> handleIgnoredUsers(it) is UserAccountDataIgnoredUsers -> handleIgnoredUsers(it)
is UserAccountDataBreadcrumbs -> handleBreadcrumbs(it)
is UserAccountDataFallback -> Timber.d("Receive account data of unhandled type ${it.type}") is UserAccountDataFallback -> Timber.d("Receive account data of unhandled type ${it.type}")
else -> error("Missing code here!") else -> error("Missing code here!")
} }
@ -130,4 +133,10 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
.executeBy(taskExecutor) .executeBy(taskExecutor)
// TODO If not initial sync, we should execute a init sync // 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)
}
} }

View file

@ -25,6 +25,7 @@ internal abstract class UserAccountData {
companion object { companion object {
const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list" const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list"
const val TYPE_DIRECT_MESSAGES = "m.direct" 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_PREVIEW_URLS = "org.matrix.preview_urls"
const val TYPE_WIDGETS = "m.widgets" const val TYPE_WIDGETS = "m.widgets"
const val TYPE_PUSH_RULES = "m.push_rules" const val TYPE_PUSH_RULES = "m.push_rules"

View file

@ -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<String> = emptyList()
)

View file

@ -35,5 +35,11 @@ internal abstract class AccountDataModule {
} }
@Binds @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
} }

View file

@ -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<SaveBreadcrumbsTask.Params, Unit> {
data class Params(
val recentRoomIds: List<String>
)
}
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<String>().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
}
}
}
}

View file

@ -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<UpdateBreadcrumbsTask.Params, Unit> {
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)
))
}
}

View file

@ -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.di.UserId
import im.vector.matrix.android.internal.network.executeRequest 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.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject import javax.inject.Inject
@ -38,6 +39,15 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
return directMessages return directMessages
} }
} }
data class BreadcrumbsParams(override val type: String = UserAccountData.TYPE_BREADCRUMBS,
private val breadcrumbsContent: BreadcrumbsContent
) : Params {
override fun getData(): Any {
return breadcrumbsContent
}
}
} }
internal class DefaultUpdateUserAccountDataTask @Inject constructor(private val accountDataApi: AccountDataAPI, internal class DefaultUpdateUserAccountDataTask @Inject constructor(private val accountDataApi: AccountDataAPI,

View file

@ -2,8 +2,19 @@
<resources> <resources>
<string name="notice_room_invite_no_invitee_with_reason">%1$s\'s invitation. Reason: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s invited %2$s. Reason: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s invited you. Reason: %2$s</string>
<string name="notice_room_join_with_reason">%1$s joined. Reason: %2$s</string>
<string name="notice_room_leave_with_reason">%1$s left. Reason: %2$s</string>
<string name="notice_room_reject_with_reason">%1$s rejected the invitation. Reason: %2$s</string>
<string name="notice_room_kick_with_reason">%1$s kicked %2$s. Reason: %3$s</string>
<string name="notice_room_unban_with_reason">%1$s unbanned %2$s. Reason: %3$s</string>
<string name="notice_room_ban_with_reason">%1$s banned %2$s. Reason: %3$s</string>
<string name="notice_room_third_party_invite_with_reason">%1$s sent an invitation to %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s revoked the invitation for %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s accepted the invitation for %2$s. Reason: %3$s</string>
<string name="notice_room_withdraw_with_reason">%1$s withdrew %2$s\'s invitation. Reason: %3$s</string>
<string name="no_network_indicator">There is no network connection right now</string> <string name="no_network_indicator">There is no network connection right now</string>
</resources> </resources>

View file

@ -82,3 +82,6 @@ layout_constraintLeft_
### Will crash on API < 21. Use ?colorAccent instead ### Will crash on API < 21. Use ?colorAccent instead
\?android:colorAccent \?android:colorAccent
\?android:attr/colorAccent \?android:attr/colorAccent
### Use androidx.recyclerview.widget.RecyclerView because EpoxyRecyclerViews add behavior we do not want to
<com\.airbnb\.epoxy\.EpoxyRecyclerView

View file

@ -15,8 +15,8 @@ androidExtensions {
} }
ext.versionMajor = 0 ext.versionMajor = 0
ext.versionMinor = 9 ext.versionMinor = 10
ext.versionPatch = 1 ext.versionPatch = 0
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'

View file

@ -20,16 +20,22 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import im.vector.matrix.android.api.crypto.getAllVerificationEmojis import im.vector.matrix.android.api.crypto.getAllVerificationEmojis
import im.vector.riotx.R import im.vector.riotx.R
import kotlinx.android.synthetic.main.fragment_generic_recycler_epoxy.* import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
class DebugSasEmojiActivity : AppCompatActivity() { class DebugSasEmojiActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.fragment_generic_recycler_epoxy) setContentView(R.layout.fragment_generic_recycler)
val controller = SasEmojiController() val controller = SasEmojiController()
epoxyRecyclerView.setController(controller) recyclerView.configureWith(controller)
controller.setData(SasState(getAllVerificationEmojis())) controller.setData(SasState(getAllVerificationEmojis()))
} }
override fun onDestroy() {
recyclerView.cleanup()
super.onDestroy()
}
} }

View file

@ -42,7 +42,7 @@ import im.vector.riotx.core.di.DaggerVectorComponent
import im.vector.riotx.core.di.HasVectorInjector import im.vector.riotx.core.di.HasVectorInjector
import im.vector.riotx.core.di.VectorComponent import im.vector.riotx.core.di.VectorComponent
import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.utils.initKnownEmojiHashSet import im.vector.riotx.core.rx.setupRxPlugin
import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
@ -55,8 +55,7 @@ import im.vector.riotx.features.version.VersionProvider
import im.vector.riotx.push.fcm.FcmHelper import im.vector.riotx.push.fcm.FcmHelper
import timber.log.Timber import timber.log.Timber
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.*
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider { class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider {
@ -79,14 +78,13 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
lateinit var vectorComponent: VectorComponent lateinit var vectorComponent: VectorComponent
private var fontThreadHandler: Handler? = null private var fontThreadHandler: Handler? = null
// var slowMode = false
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
appContext = this appContext = this
vectorComponent = DaggerVectorComponent.factory().create(this) vectorComponent = DaggerVectorComponent.factory().create(this)
vectorComponent.inject(this) vectorComponent.inject(this)
vectorUncaughtExceptionHandler.activate(this) vectorUncaughtExceptionHandler.activate(this)
setupRxPlugin()
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
@ -138,7 +136,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
}) })
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler) ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
// This should be done as early as possible // This should be done as early as possible
initKnownEmojiHashSet(appContext) // initKnownEmojiHashSet(appContext)
} }
override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION) override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION)

View file

@ -33,10 +33,12 @@ import im.vector.riotx.features.home.LoadingFragment
import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsersFragment import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsersFragment
import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFragment
import im.vector.riotx.features.home.group.GroupListFragment import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.breadcrumbs.BreadcrumbsFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.list.RoomListFragment import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.login.* import im.vector.riotx.features.login.*
import im.vector.riotx.features.login.terms.LoginTermsFragment import im.vector.riotx.features.login.terms.LoginTermsFragment
import im.vector.riotx.features.reactions.EmojiChooserFragment
import im.vector.riotx.features.reactions.EmojiSearchResultFragment import im.vector.riotx.features.reactions.EmojiSearchResultFragment
import im.vector.riotx.features.roomdirectory.PublicRoomsFragment import im.vector.riotx.features.roomdirectory.PublicRoomsFragment
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
@ -249,4 +251,14 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(PublicRoomsFragment::class) @FragmentKey(PublicRoomsFragment::class)
fun bindPublicRoomsFragment(fragment: PublicRoomsFragment): Fragment fun bindPublicRoomsFragment(fragment: PublicRoomsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(BreadcrumbsFragment::class)
fun bindBreadcrumbsFragment(fragment: BreadcrumbsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(EmojiChooserFragment::class)
fun bindEmojiChooserFragment(fragment: EmojiChooserFragment): Fragment
} }

View file

@ -29,6 +29,7 @@ import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedVie
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
import im.vector.riotx.features.home.HomeSharedActionViewModel import im.vector.riotx.features.home.HomeSharedActionViewModel
import im.vector.riotx.features.home.createdirect.CreateDirectRoomSharedActionViewModel import im.vector.riotx.features.home.createdirect.CreateDirectRoomSharedActionViewModel
import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel
import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.riotx.features.login.LoginSharedActionViewModel import im.vector.riotx.features.login.LoginSharedActionViewModel
@ -118,4 +119,9 @@ interface ViewModelModule {
@IntoMap @IntoMap
@ViewModelKey(LoginSharedActionViewModel::class) @ViewModelKey(LoginSharedActionViewModel::class)
fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(RoomDetailSharedActionViewModel::class)
fun bindRoomDetailSharedActionViewModel(viewModel: RoomDetailSharedActionViewModel): ViewModel
} }

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import java.net.HttpURLConnection
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
@ -76,6 +77,15 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
} }
} }
} }
is Failure.OtherServerError -> {
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 else -> throwable.localizedMessage
} }
?: stringProvider.getString(R.string.unknown_error) ?: stringProvider.getString(R.string.unknown_error)

View file

@ -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
}

View file

@ -51,7 +51,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
init { init {
View.inflate(context, R.layout.view_state, this) 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 { errorRetryView.setOnClickListener {
eventCallback?.onRetryClicked() eventCallback?.onRetryClicked()
} }

View file

@ -73,7 +73,7 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
injectWith(screenComponent) injectWith(screenComponent)
} }
protected open fun injectWith(screenComponent: ScreenComponent) = Unit protected open fun injectWith(injector: ScreenComponent) = Unit
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
mvrxViewIdProperty.restoreFrom(savedInstanceState) mvrxViewIdProperty.restoreFrom(savedInstanceState)

View file

@ -135,6 +135,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
restorables.forEach { it.onSaveInstanceState(outState) } restorables.forEach { it.onSaveInstanceState(outState) }
restorables.clear()
} }
override fun onViewStateRestored(savedInstanceState: Bundle?) { override fun onViewStateRestored(savedInstanceState: Bundle?) {

View file

@ -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
}
}
}

View file

@ -16,13 +16,6 @@
package im.vector.riotx.core.utils 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 import java.util.regex.Pattern
private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" + 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?" + "|\uD83C\uDCCF\uFE0F?" +
"|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))") "|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))")
/*
// A hashset from all supported emoji // A hashset from all supported emoji
private var knownEmojiSet: HashSet<String>? = null private var knownEmojiSet: HashSet<String>? = null
@ -56,7 +50,7 @@ fun initKnownEmojiHashSet(context: Context, done: (() -> Unit)? = null) {
GlobalScope.launch { GlobalScope.launch {
context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input -> context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input ->
val moshi = Moshi.Builder().build() 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 inputAsString = input.bufferedReader().use { it.readText() }
val source = jsonAdapter.fromJson(inputAsString) val source = jsonAdapter.fromJson(inputAsString)
knownEmojiSet = HashSet<String>().also { knownEmojiSet = HashSet<String>().also {
@ -77,6 +71,7 @@ fun isSingleEmoji(string: String): Boolean {
} }
return knownEmojiSet?.contains(string) ?: false return knownEmojiSet?.contains(string) ?: false
} }
*/
/** /**
* Test if a string contains emojis. * Test if a string contains emojis.

View file

@ -27,16 +27,19 @@ import im.vector.riotx.R
enum class Command(val command: String, val parameters: String, @StringRes val description: Int) { enum class Command(val command: String, val parameters: String, @StringRes val description: Int) {
EMOTE("/me", "<message>", R.string.command_description_emote), EMOTE("/me", "<message>", R.string.command_description_emote),
BAN_USER("/ban", "<user-id> [reason]", R.string.command_description_ban_user), BAN_USER("/ban", "<user-id> [reason]", R.string.command_description_ban_user),
UNBAN_USER("/unban", "<user-id>", R.string.command_description_unban_user), UNBAN_USER("/unban", "<user-id> [reason]", R.string.command_description_unban_user),
SET_USER_POWER_LEVEL("/op", "<user-id> [<power-level>]", R.string.command_description_op_user), SET_USER_POWER_LEVEL("/op", "<user-id> [<power-level>]", R.string.command_description_op_user),
RESET_USER_POWER_LEVEL("/deop", "<user-id>", R.string.command_description_deop_user), RESET_USER_POWER_LEVEL("/deop", "<user-id>", R.string.command_description_deop_user),
INVITE("/invite", "<user-id>", R.string.command_description_invite_user), INVITE("/invite", "<user-id> [reason]", R.string.command_description_invite_user),
JOIN_ROOM("/join", "<room-alias>", R.string.command_description_join_room), JOIN_ROOM("/join", "<room-alias> [reason]", R.string.command_description_join_room),
PART("/part", "<room-alias>", R.string.command_description_part_room), PART("/part", "<room-alias> [reason]", R.string.command_description_part_room),
TOPIC("/topic", "<topic>", R.string.command_description_topic), TOPIC("/topic", "<topic>", R.string.command_description_topic),
KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user), KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user),
CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick), CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick),
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown), MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown),
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token), CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token),
SPOILER("/spoiler", "<message>", R.string.command_description_spoiler); SPOILER("/spoiler", "<message>", R.string.command_description_spoiler);
val length
get() = command.length + 1
} }

View file

@ -81,29 +81,52 @@ object CommandParser {
ParsedCommand.SendEmote(message) ParsedCommand.SendEmote(message)
} }
Command.JOIN_ROOM.command -> { 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()) { if (roomAlias.isNotEmpty()) {
ParsedCommand.JoinRoom(roomAlias) ParsedCommand.JoinRoom(
roomAlias,
textMessage.substring(Command.JOIN_ROOM.length + roomAlias.length)
.trim()
.takeIf { it.isNotBlank() }
)
} else {
ParsedCommand.ErrorSyntax(Command.JOIN_ROOM)
}
} else { } else {
ParsedCommand.ErrorSyntax(Command.JOIN_ROOM) ParsedCommand.ErrorSyntax(Command.JOIN_ROOM)
} }
} }
Command.PART.command -> { Command.PART.command -> {
val roomAlias = textMessage.substring(Command.PART.command.length).trim() if (messageParts.size >= 2) {
val roomAlias = messageParts[1]
if (roomAlias.isNotEmpty()) { if (roomAlias.isNotEmpty()) {
ParsedCommand.PartRoom(roomAlias) ParsedCommand.PartRoom(
roomAlias,
textMessage.substring(Command.PART.length + roomAlias.length)
.trim()
.takeIf { it.isNotBlank() }
)
} else {
ParsedCommand.ErrorSyntax(Command.PART)
}
} else { } else {
ParsedCommand.ErrorSyntax(Command.PART) ParsedCommand.ErrorSyntax(Command.PART)
} }
} }
Command.INVITE.command -> { Command.INVITE.command -> {
if (messageParts.size == 2) { if (messageParts.size >= 2) {
val userId = messageParts[1] val userId = messageParts[1]
if (MatrixPatterns.isUserId(userId)) { if (MatrixPatterns.isUserId(userId)) {
ParsedCommand.Invite(userId) ParsedCommand.Invite(
userId,
textMessage.substring(Command.INVITE.length + userId.length)
.trim()
.takeIf { it.isNotBlank() }
)
} else { } else {
ParsedCommand.ErrorSyntax(Command.INVITE) ParsedCommand.ErrorSyntax(Command.INVITE)
} }
@ -114,12 +137,14 @@ object CommandParser {
Command.KICK_USER.command -> { Command.KICK_USER.command -> {
if (messageParts.size >= 2) { if (messageParts.size >= 2) {
val userId = messageParts[1] 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 { } else {
ParsedCommand.ErrorSyntax(Command.KICK_USER) ParsedCommand.ErrorSyntax(Command.KICK_USER)
} }
@ -130,12 +155,14 @@ object CommandParser {
Command.BAN_USER.command -> { Command.BAN_USER.command -> {
if (messageParts.size >= 2) { if (messageParts.size >= 2) {
val userId = messageParts[1] 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 { } else {
ParsedCommand.ErrorSyntax(Command.BAN_USER) ParsedCommand.ErrorSyntax(Command.BAN_USER)
} }
@ -144,11 +171,16 @@ object CommandParser {
} }
} }
Command.UNBAN_USER.command -> { Command.UNBAN_USER.command -> {
if (messageParts.size == 2) { if (messageParts.size >= 2) {
val userId = messageParts[1] val userId = messageParts[1]
if (MatrixPatterns.isUserId(userId)) { if (MatrixPatterns.isUserId(userId)) {
ParsedCommand.UnbanUser(userId) ParsedCommand.UnbanUser(
userId,
textMessage.substring(Command.UNBAN_USER.length + userId.length)
.trim()
.takeIf { it.isNotBlank() }
)
} else { } else {
ParsedCommand.ErrorSyntax(Command.UNBAN_USER) ParsedCommand.ErrorSyntax(Command.UNBAN_USER)
} }

View file

@ -34,14 +34,14 @@ sealed class ParsedCommand {
// Valid commands: // Valid commands:
class SendEmote(val message: CharSequence) : ParsedCommand() class SendEmote(val message: CharSequence) : ParsedCommand()
class BanUser(val userId: String, val reason: String) : ParsedCommand() class BanUser(val userId: String, val reason: String?) : ParsedCommand()
class UnbanUser(val userId: String) : ParsedCommand() class UnbanUser(val userId: String, val reason: String?) : ParsedCommand()
class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand() class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand()
class Invite(val userId: String) : ParsedCommand() class Invite(val userId: String, val reason: String?) : ParsedCommand()
class JoinRoom(val roomAlias: String) : ParsedCommand() class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
class PartRoom(val roomAlias: String) : ParsedCommand() class PartRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
class ChangeTopic(val topic: 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 ChangeDisplayName(val displayName: String) : ParsedCommand()
class SetMarkdown(val enable: Boolean) : ParsedCommand() class SetMarkdown(val enable: Boolean) : ParsedCommand()
object ClearScalarToken : ParsedCommand() object ClearScalarToken : ParsedCommand()

View file

@ -21,6 +21,8 @@ import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R 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.platform.VectorBaseFragment
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
keysBackupSettingsRecyclerView.configureWith(keysBackupSettingsRecyclerViewController)
keysBackupSettingsRecyclerView.setController(keysBackupSettingsRecyclerViewController)
keysBackupSettingsRecyclerViewController.listener = this keysBackupSettingsRecyclerViewController.listener = this
} }
override fun onDestroyView() {
keysBackupSettingsRecyclerViewController.listener = null
keysBackupSettingsRecyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
keysBackupSettingsRecyclerViewController.setData(state) keysBackupSettingsRecyclerViewController.setData(state)
} }

View file

@ -25,7 +25,6 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView 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.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.R import im.vector.riotx.R
@ -46,7 +45,6 @@ private const val INDEX_PEOPLE = 1
private const val INDEX_ROOMS = 2 private const val INDEX_ROOMS = 2
class HomeDetailFragment @Inject constructor( class HomeDetailFragment @Inject constructor(
private val session: Session,
val homeDetailViewModelFactory: HomeDetailViewModel.Factory, val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
private val avatarRenderer: AvatarRenderer private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment(), KeysBackupBanner.Delegate { ) : VectorBaseFragment(), KeysBackupBanner.Delegate {
@ -56,9 +54,7 @@ class HomeDetailFragment @Inject constructor(
private val viewModel: HomeDetailViewModel by fragmentViewModel() private val viewModel: HomeDetailViewModel by fragmentViewModel()
private lateinit var sharedActionViewModel: HomeSharedActionViewModel private lateinit var sharedActionViewModel: HomeSharedActionViewModel
override fun getLayoutResId(): Int { override fun getLayoutResId() = R.layout.fragment_home_detail
return R.layout.fragment_home_detail
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View file

@ -23,9 +23,7 @@ import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.*
import im.vector.riotx.core.extensions.setupAsSearch
import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.* import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.*
import javax.inject.Inject import javax.inject.Inject
@ -48,10 +46,15 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor(
setupCloseView() setupCloseView()
} }
override fun onDestroyView() {
recyclerView.cleanup()
directRoomController.callback = null
super.onDestroyView()
}
private fun setupRecyclerView() { private fun setupRecyclerView() {
recyclerView.setHasFixedSize(true)
directRoomController.callback = this directRoomController.callback = this
recyclerView.setController(directRoomController) recyclerView.configureWith(directRoomController)
} }
private fun setupSearchByMatrixIdView() { private fun setupSearchByMatrixIdView() {

View file

@ -31,9 +31,7 @@ import com.google.android.material.chip.ChipGroup
import com.jakewharton.rxbinding3.widget.textChanges import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.*
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setupAsSearch
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.DimensionConverter
import kotlinx.android.synthetic.main.fragment_create_direct_room.* 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) { override fun onPrepareOptionsMenu(menu: Menu) {
withState(viewModel) { withState(viewModel) {
val createMenuItem = menu.findItem(R.id.action_create_direct_room) val createMenuItem = menu.findItem(R.id.action_create_direct_room)
@ -94,11 +98,10 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
recyclerView.setHasFixedSize(true)
// Don't activate animation as we might have way to much item animation when filtering // Don't activate animation as we might have way to much item animation when filtering
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
knownUsersController.callback = this knownUsersController.callback = this
recyclerView.setController(knownUsersController) recyclerView.configureWith(knownUsersController)
} }
private fun setupFilterView() { private fun setupFilterView() {

View file

@ -23,11 +23,13 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotx.R 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.extensions.observeEvent
import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment 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.HomeActivitySharedAction
import im.vector.riotx.features.home.HomeSharedActionViewModel
import kotlinx.android.synthetic.main.fragment_group_list.* import kotlinx.android.synthetic.main.fragment_group_list.*
import javax.inject.Inject import javax.inject.Inject
@ -45,14 +47,20 @@ class GroupListFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
groupController.callback = this groupController.callback = this
stateView.contentView = groupListEpoxyRecyclerView stateView.contentView = groupListView
groupListEpoxyRecyclerView.setController(groupController) groupListView.configureWith(groupController)
viewModel.subscribe { renderState(it) } viewModel.subscribe { renderState(it) }
viewModel.openGroupLiveData.observeEvent(this) { viewModel.openGroupLiveData.observeEvent(this) {
sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup) sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup)
} }
} }
override fun onDestroyView() {
groupController.callback = null
groupListView.cleanup()
super.onDestroyView()
}
private fun renderState(state: GroupListViewState) { private fun renderState(state: GroupListViewState) {
when (state.asyncGroups) { when (state.asyncGroups) {
is Incomplete -> stateView.state = StateView.State.Loading is Incomplete -> stateView.state = StateView.State.Loading

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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))
}
}

View file

@ -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<BreadcrumbsItem.Holder>() {
@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<UnreadCounterBadgeView>(R.id.breadcrumbsUnreadCounterBadgeView)
val unreadIndentIndicator by bind<View>(R.id.breadcrumbsUnreadIndicator)
val draftIndentIndicator by bind<View>(R.id.breadcrumbsDraftBadge)
val avatarImageView by bind<ImageView>(R.id.breadcrumbsImageView)
val rootView by bind<ViewGroup>(R.id.breadcrumbsRoot)
}
}

View file

@ -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<BreadcrumbsViewState, EmptyAction>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: BreadcrumbsViewState): BreadcrumbsViewModel
}
companion object : MvRxViewModelFactory<BreadcrumbsViewModel, BreadcrumbsViewState> {
@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)
}
}
}

View file

@ -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<List<RoomSummary>> = Uninitialized
) : MvRxState

View file

@ -20,17 +20,25 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity 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.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable { class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
override fun getLayoutRes(): Int { override fun getLayoutRes() = R.layout.activity_room_detail
return R.layout.activity_room_detail
} private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel
// Simple filter
private var currentRoomId: String? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -38,14 +46,57 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
if (isFirstCreation()) { if (isFirstCreation()) {
val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS) val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS)
?: return ?: return
currentRoomId = roomDetailArgs.roomId
replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs) 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) { override fun configure(toolbar: Toolbar) {
configureToolbar(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 { companion object {
private const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS" private const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS"

View file

@ -46,6 +46,7 @@ import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.* import com.airbnb.mvrx.*
import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader 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.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.*
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
@ -193,6 +191,8 @@ class RoomDetailFragment @Inject constructor(
private lateinit var sharedActionViewModel: MessageSharedActionViewModel private lateinit var sharedActionViewModel: MessageSharedActionViewModel
private lateinit var layoutManager: LinearLayoutManager private lateinit var layoutManager: LinearLayoutManager
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var attachmentsHelper: AttachmentsHelper private lateinit var attachmentsHelper: AttachmentsHelper
private lateinit var keyboardStateUtils: KeyboardStateUtils private lateinit var keyboardStateUtils: KeyboardStateUtils
@ -286,13 +286,16 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onDestroyView() { override fun onDestroyView() {
timelineEventController.callback = null
timelineEventController.removeModelBuildListener(modelBuildListener)
modelBuildListener = null
debouncer.cancelAll()
recyclerView.cleanup()
super.onDestroyView() super.onDestroyView()
recyclerView.adapter = null
} }
override fun onDestroy() { override fun onDestroy() {
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
debouncer.cancelAll()
super.onDestroy() super.onDestroy()
} }
@ -447,11 +450,7 @@ class RoomDetailFragment @Inject constructor(
if (!hasBeenHandled && resultCode == RESULT_OK && data != null) { if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
when (requestCode) { when (requestCode) {
REACTION_SELECT_REQUEST_CODE -> { REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return
?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return
// TODO check if already reacted with that?
roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction)) roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction))
} }
} }
@ -470,13 +469,14 @@ class RoomDetailFragment @Inject constructor(
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
timelineEventController.addModelBuildListener { modelBuildListener = OnModelBuildFinishedListener {
it.dispatchTo(stateRestorer) it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnNewMessageCallback)
it.dispatchTo(scrollOnHighlightedEventCallback) it.dispatchTo(scrollOnHighlightedEventCallback)
updateJumpToReadMarkerViewVisibility() updateJumpToReadMarkerViewVisibility()
updateJumpToBottomViewVisibility() updateJumpToBottomViewVisibility()
} }
timelineEventController.addModelBuildListener(modelBuildListener)
recyclerView.adapter = timelineEventController.adapter recyclerView.adapter = timelineEventController.adapter
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
@ -521,27 +521,29 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun updateJumpToReadMarkerViewVisibility() = jumpToReadMarkerView.post { private fun updateJumpToReadMarkerViewVisibility() {
withState(roomDetailViewModel) { jumpToReadMarkerView?.post {
val showJumpToUnreadBanner = when (it.unreadState) { withState(roomDetailViewModel) {
UnreadState.Unknown, val showJumpToUnreadBanner = when (it.unreadState) {
UnreadState.HasNoUnread -> false UnreadState.Unknown,
is UnreadState.ReadMarkerNotLoaded -> true UnreadState.HasNoUnread -> false
is UnreadState.HasUnread -> { is UnreadState.ReadMarkerNotLoaded -> true
if (it.canShowJumpToReadMarker) { is UnreadState.HasUnread -> {
val lastVisibleItem = layoutManager.findLastVisibleItemPosition() if (it.canShowJumpToReadMarker) {
val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
if (positionOfReadMarker == null) { val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
false if (positionOfReadMarker == null) {
false
} else {
positionOfReadMarker > lastVisibleItem
}
} else { } else {
positionOfReadMarker > lastVisibleItem false
} }
} else {
false
} }
} }
jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
} }
jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
} }
} }
@ -1181,7 +1183,7 @@ class RoomDetailFragment @Inject constructor(
&& userId == session.myUserId) { && userId == session.myUserId) {
// Empty composer, current user: start an emote // Empty composer, current user: start an emote
composerLayout.composerEditText.setText(Command.EMOTE.command + " ") composerLayout.composerEditText.setText(Command.EMOTE.command + " ")
composerLayout.composerEditText.setSelection(Command.EMOTE.command.length + 1) composerLayout.composerEditText.setSelection(Command.EMOTE.length)
} else { } else {
val roomMember = roomDetailViewModel.getMember(userId) val roomMember = roomDetailViewModel.getMember(userId)
// TODO move logic outside of fragment // TODO move logic outside of fragment

View file

@ -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()
}

View file

@ -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<RoomDetailSharedAction>()

View file

@ -143,6 +143,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
timeline.addListener(this) timeline.addListener(this)
timeline.start() timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) } setState { copy(timeline = this@RoomDetailViewModel.timeline) }
// Inform the SDK that the room is displayed
session.onRoomDisplayed(initialState.roomId)
} }
override fun handle(action: RoomDetailAction) { override fun handle(action: RoomDetailAction) {
@ -197,9 +200,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
invisibleEventsObservable.accept(action) invisibleEventsObservable.accept(action)
} }
fun getMember(userId: String) : RoomMember? { fun getMember(userId: String): RoomMember? {
return room.getRoomMember(userId) return room.getRoomMember(userId)
} }
/** /**
* Convert a send mode to a draft and save the draft * Convert a send mode to a draft and save the draft
*/ */
@ -263,7 +267,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
} }
session.rx() session.rx()
.joinRoom(roomId, viaServer) .joinRoom(roomId, viaServers = viaServer)
.map { roomId } .map { roomId }
.execute { .execute {
copy(tombstoneEventHandling = it) copy(tombstoneEventHandling = it)
@ -484,7 +488,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) { private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled()) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
room.invite(invite.userId, object : MatrixCallback<Unit> { room.invite(invite.userId, invite.reason, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk) _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk)
} }
@ -550,7 +554,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
private fun handleRejectInvite() { private fun handleRejectInvite() {
room.leave(object : MatrixCallback<Unit> {}) room.leave(null, object : MatrixCallback<Unit> {})
} }
private fun handleAcceptInvite() { private fun handleAcceptInvite() {
@ -859,7 +863,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
override fun onCleared() { override fun onCleared() {
timeline.dispose() timeline.dispose()
timeline.removeAllListeners() timeline.removeListener(this)
super.onCleared() super.onCleared()
} }
} }

View file

@ -21,7 +21,6 @@ import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -29,6 +28,8 @@ import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent 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.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@ -52,8 +53,8 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
private val displayReadReceiptArgs: DisplayReadReceiptArgs by args() private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
screenComponent.inject(this) injector.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -64,12 +65,16 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) recyclerView.configureWith(epoxyController, hasFixedSize = false)
recyclerView.adapter = epoxyController.adapter
bottomSheetTitle.text = getString(R.string.seen_by) bottomSheetTitle.text = getString(R.string.seen_by)
epoxyController.setData(displayReadReceiptArgs.readReceipts) 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() // we are not using state for this one as it's static, so no need to override invalidate()
companion object { companion object {

View file

@ -25,7 +25,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState 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.model.message.*
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
@ -44,7 +43,7 @@ import org.threeten.bp.LocalDateTime
import javax.inject.Inject import javax.inject.Inject
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
private val session: Session, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val timelineItemFactory: TimelineItemFactory, private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val mergedHeaderItemFactory: MergedHeaderItemFactory, private val mergedHeaderItemFactory: MergedHeaderItemFactory,
@ -209,6 +208,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
timelineMediaSizeProvider.recyclerView = recyclerView timelineMediaSizeProvider.recyclerView = recyclerView
} }
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
timelineMediaSizeProvider.recyclerView = null
contentUploadStateTrackerBinder.clear()
timeline?.removeListener(this)
super.onDetachedFromRecyclerView(recyclerView)
}
override fun buildModels() { override fun buildModels() {
val timestamp = System.currentTimeMillis() val timestamp = System.currentTimeMillis()
showingForwardLoader = LoadingItem_() showingForwardLoader = LoadingItem_()

View file

@ -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
}
}

View file

@ -19,7 +19,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -27,6 +26,8 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent 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.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import javax.inject.Inject import javax.inject.Inject
@ -48,8 +49,8 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
private lateinit var sharedActionViewModel: MessageSharedActionViewModel private lateinit var sharedActionViewModel: MessageSharedActionViewModel
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
screenComponent.inject(this) injector.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -61,13 +62,17 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) recyclerView.configureWith(messageActionsEpoxyController, hasFixedSize = false)
recyclerView.adapter = messageActionsEpoxyController.adapter
// Disable item animation // Disable item animation
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
messageActionsEpoxyController.listener = this messageActionsEpoxyController.listener = this
} }
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun onUrlClicked(url: String): Boolean { override fun onUrlClicked(url: String): Boolean {
sharedActionViewModel.post(EventSharedAction.OnUrlClicked(url)) sharedActionViewModel.post(EventSharedAction.OnUrlClicked(url))
// Always consume // Always consume
@ -83,6 +88,10 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
override fun didSelectMenuAction(eventAction: EventSharedAction) { override fun didSelectMenuAction(eventAction: EventSharedAction) {
if (eventAction is EventSharedAction.ReportContent) { if (eventAction is EventSharedAction.ReportContent) {
// Toggle report menu // Toggle report menu
// Enable item animation
if (recyclerView.itemAnimator == null) {
recyclerView.itemAnimator = MessageActionsAnimator()
}
viewModel.handle(MessageActionsAction.ToggleReportMenu) viewModel.handle(MessageActionsAction.ToggleReportMenu)
} else { } else {
sharedActionViewModel.post(eventAction) sharedActionViewModel.post(eventAction)

View file

@ -19,9 +19,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -30,6 +27,8 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent 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.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
@ -54,8 +53,8 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer) ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
} }
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
screenComponent.inject(this) injector.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -66,13 +65,18 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
recyclerView.adapter = epoxyController.adapter recyclerView.configureWith(
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) epoxyController,
val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayout.VERTICAL) showDivider = true,
recyclerView.addItemDecoration(dividerItemDecoration) hasFixedSize = false)
bottomSheetTitle.text = context?.getString(R.string.message_edits) bottomSheetTitle.text = context?.getString(R.string.message_edits)
} }
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
epoxyController.setData(it) epoxyController.setData(it)
super.invalidate() super.invalidate()

View file

@ -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.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.ui.list.genericFooterItem import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItem import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemHeader import im.vector.riotx.core.ui.list.genericItemHeader
import im.vector.riotx.core.ui.list.genericLoaderItem import im.vector.riotx.core.ui.list.genericLoaderItem
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import me.gujun.android.span.span import me.gujun.android.span.span
import name.fraser.neil.plaintext.diff_match_patch import name.fraser.neil.plaintext.diff_match_patch
import timber.log.Timber import java.util.*
import java.util.Calendar
/** /**
* Epoxy controller for edit history list * Epoxy controller for edit history list
@ -104,9 +103,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
?: nContent.first ?: nContent.first
val dmp = diff_match_patch() val dmp = diff_match_patch()
val diff = dmp.diff_main(nextBody.toString(), body.toString()) val diff = dmp.diff_main(nextBody.toString(), body.toString())
Timber.e("#### Diff: $diff")
dmp.diff_cleanupSemantic(diff) dmp.diff_cleanupSemantic(diff)
Timber.e("#### Diff: $diff")
spannedDiff = span { spannedDiff = span {
diff.map { diff.map {
when (it.operation) { when (it.operation) {

View file

@ -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.Event
import im.vector.matrix.android.api.session.events.model.EventType 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.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.*
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.call.CallInviteContent import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.R import im.vector.riotx.R
@ -36,7 +29,7 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class NoticeEventFormatter @Inject constructor(private val sessionHolder: ActiveSessionHolder, class NoticeEventFormatter @Inject constructor(private val sessionHolder: ActiveSessionHolder,
private val stringProvider: StringProvider) { private val sp: StringProvider) {
fun format(timelineEvent: TimelineEvent): CharSequence? { fun format(timelineEvent: TimelineEvent): CharSequence? {
return when (val type = timelineEvent.root.getClearType()) { 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? { private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
val content = event.getClearContent().toModel<RoomNameContent>() ?: return null val content = event.getClearContent().toModel<RoomNameContent>() ?: return null
return if (content.name.isNullOrBlank()) { return if (content.name.isNullOrBlank()) {
stringProvider.getString(R.string.notice_room_name_removed, senderName) sp.getString(R.string.notice_room_name_removed, senderName)
} else { } 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? { 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? { private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? {
val content = event.getClearContent().toModel<RoomTopicContent>() ?: return null val content = event.getClearContent().toModel<RoomTopicContent>() ?: return null
return if (content.topic.isNullOrEmpty()) { return if (content.topic.isNullOrEmpty()) {
stringProvider.getString(R.string.notice_room_topic_removed, senderName) sp.getString(R.string.notice_room_topic_removed, senderName)
} else { } 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? { private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
?: return null
val formattedVisibility = when (historyVisibility) { val formattedVisibility = when (historyVisibility) {
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) RoomHistoryVisibility.SHARED -> sp.getString(R.string.notice_room_visibility_shared)
RoomHistoryVisibility.INVITED -> stringProvider.getString(R.string.notice_room_visibility_invited) RoomHistoryVisibility.INVITED -> sp.getString(R.string.notice_room_visibility_invited)
RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) RoomHistoryVisibility.JOINED -> sp.getString(R.string.notice_room_visibility_joined)
RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) 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? { 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<CallInviteContent>() ?: return null val content = event.getClearContent().toModel<CallInviteContent>() ?: return null
val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
return if (isVideoCall) { return if (isVideoCall) {
stringProvider.getString(R.string.notice_placed_video_call, senderName) sp.getString(R.string.notice_placed_video_call, senderName)
} else { } 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_ANSWER == event.type -> sp.getString(R.string.notice_answered_call, senderName)
EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, senderName) EventType.CALL_HANGUP == event.type -> sp.getString(R.string.notice_ended_call, senderName)
else -> null else -> null
} }
} }
@ -150,12 +142,11 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
if (eventContent?.displayName != prevEventContent?.displayName) { if (eventContent?.displayName != prevEventContent?.displayName) {
val displayNameText = when { val displayNameText = when {
prevEventContent?.displayName.isNullOrEmpty() -> 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() -> 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 -> else ->
stringProvider.getString(R.string.notice_display_name_changed_from, sp.getString(R.string.notice_display_name_changed_from, event.senderId, prevEventContent?.displayName, eventContent?.displayName)
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
} }
displayText.append(displayNameText) displayText.append(displayNameText)
} }
@ -163,73 +154,96 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
if (eventContent?.avatarUrl != prevEventContent?.avatarUrl) { if (eventContent?.avatarUrl != prevEventContent?.avatarUrl) {
val displayAvatarText = if (displayText.isNotEmpty()) { val displayAvatarText = if (displayText.isNotEmpty()) {
displayText.append(" ") displayText.append(" ")
stringProvider.getString(R.string.notice_avatar_changed_too) sp.getString(R.string.notice_avatar_changed_too)
} else { } else {
stringProvider.getString(R.string.notice_avatar_url_changed, senderName) sp.getString(R.string.notice_avatar_url_changed, senderName)
} }
displayText.append(displayAvatarText) displayText.append(displayAvatarText)
} }
if (displayText.isEmpty()) { if (displayText.isEmpty()) {
displayText.append( displayText.append(
stringProvider.getString(R.string.notice_member_no_changes, senderName) sp.getString(R.string.notice_member_no_changes, senderName)
) )
} }
return displayText.toString() return displayText.toString()
} }
private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? { private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val senderDisplayName = senderName ?: event.senderId val senderDisplayName = senderName ?: event.senderId ?: ""
val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: "" val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
return when { return when (eventContent?.membership) {
Membership.INVITE == eventContent?.membership -> { Membership.INVITE -> {
val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId
when { when {
eventContent.thirdPartyInvite != null -> { eventContent.thirdPartyInvite != null -> {
val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey
?: event.stateKey val threePidDisplayName = eventContent.thirdPartyInvite?.displayName ?: ""
stringProvider.getString(R.string.notice_room_third_party_registered_invite, eventContent.safeReason?.let { reason ->
userWhoHasAccepted, eventContent.thirdPartyInvite?.displayName) 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 -> 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() -> 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 -> 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 -> Membership.JOIN ->
stringProvider.getString(R.string.notice_room_join, senderDisplayName) eventContent.safeReason?.let { reason ->
Membership.LEAVE == eventContent?.membership -> 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 // 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) { 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 { } 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) { } 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) { } 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) { } 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 { } else {
null null
} }
Membership.BAN == eventContent?.membership -> Membership.BAN ->
stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName) eventContent.safeReason?.let {
Membership.KNOCK == eventContent?.membership -> sp.getString(R.string.notice_room_ban_with_reason, senderDisplayName, targetDisplayName, it)
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) } ?: sp.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
else -> null 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? { private fun formatJoinRulesEvent(event: Event, senderName: String?): CharSequence? {
val content = event.getClearContent().toModel<RoomJoinRulesContent>() ?: return null val content = event.getClearContent().toModel<RoomJoinRulesContent>() ?: return null
return when (content.joinRules) { return when (content.joinRules) {
RoomJoinRules.INVITE -> stringProvider.getString(R.string.room_join_rules_invite, senderName) RoomJoinRules.INVITE -> sp.getString(R.string.room_join_rules_invite, senderName)
RoomJoinRules.PUBLIC -> stringProvider.getString(R.string.room_join_rules_public, senderName) RoomJoinRules.PUBLIC -> sp.getString(R.string.room_join_rules_public, senderName)
else -> null else -> null
} }
} }

View file

@ -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.matrix.android.api.session.room.send.SendState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder 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.error.ErrorFormatter
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.TextUtils import im.vector.riotx.core.utils.TextUtils
import im.vector.riotx.features.ui.getMessageTextColor import im.vector.riotx.features.ui.getMessageTextColor
import javax.inject.Inject import javax.inject.Inject
@ScreenScope
class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val errorFormatter: ErrorFormatter) { private val errorFormatter: ErrorFormatter) {
@ -40,7 +42,7 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
fun bind(eventId: String, fun bind(eventId: String,
isLocalFile: Boolean, isLocalFile: Boolean,
progressLayout: ViewGroup) { progressLayout: ViewGroup) {
activeSessionHolder.getActiveSession().also { session -> activeSessionHolder.getSafeActiveSession()?.also { session ->
val uploadStateTracker = session.contentUploadProgressTracker() val uploadStateTracker = session.contentUploadProgressTracker()
val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter) val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter)
updateListeners[eventId] = updateListener updateListeners[eventId] = updateListener
@ -49,13 +51,19 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
} }
fun unbind(eventId: String) { fun unbind(eventId: String) {
activeSessionHolder.getActiveSession().also { session -> activeSessionHolder.getSafeActiveSession()?.also { session ->
val uploadStateTracker = session.contentUploadProgressTracker() val uploadStateTracker = session.contentUploadProgressTracker()
updateListeners[eventId]?.also { updateListeners[eventId]?.also {
uploadStateTracker.untrack(eventId, it) uploadStateTracker.untrack(eventId, it)
} }
} }
} }
fun clear() {
activeSessionHolder.getSafeActiveSession()?.also {
it.contentUploadProgressTracker().clear()
}
}
} }
private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,

View file

@ -19,11 +19,12 @@ package im.vector.riotx.features.home.room.detail.timeline.helper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import im.vector.riotx.core.di.ScreenScope import im.vector.riotx.core.di.ScreenScope
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt
@ScreenScope @ScreenScope
class TimelineMediaSizeProvider @Inject constructor() { class TimelineMediaSizeProvider @Inject constructor() {
lateinit var recyclerView: RecyclerView var recyclerView: RecyclerView? = null
private var cachedSize: Pair<Int, Int>? = null private var cachedSize: Pair<Int, Int>? = null
fun getMaxSize(): Pair<Int, Int> { fun getMaxSize(): Pair<Int, Int> {
@ -31,17 +32,17 @@ class TimelineMediaSizeProvider @Inject constructor() {
} }
private fun computeMaxSize(): Pair<Int, Int> { private fun computeMaxSize(): Pair<Int, Int> {
val width = recyclerView.width val width = recyclerView?.width ?: 0
val height = recyclerView.height val height = recyclerView?.height ?: 0
val maxImageWidth: Int val maxImageWidth: Int
val maxImageHeight: Int val maxImageHeight: Int
// landscape / portrait // landscape / portrait
if (width < height) { if (width < height) {
maxImageWidth = Math.round(width * 0.7f) maxImageWidth = (width * 0.7f).roundToInt()
maxImageHeight = Math.round(height * 0.5f) maxImageHeight = (height * 0.5f).roundToInt()
} else { } else {
maxImageWidth = Math.round(width * 0.5f) maxImageWidth = (width * 0.5f).roundToInt()
maxImageHeight = Math.round(height * 0.7f) maxImageHeight = (height * 0.7f).roundToInt()
} }
return Pair(maxImageWidth, maxImageHeight) return Pair(maxImageWidth, maxImageHeight)
} }

View file

@ -20,7 +20,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -29,6 +28,8 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent 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.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
@ -49,8 +50,8 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Inject lateinit var epoxyController: ViewReactionsEpoxyController @Inject lateinit var epoxyController: ViewReactionsEpoxyController
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
screenComponent.inject(this) injector.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -61,11 +62,15 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) recyclerView.configureWith(epoxyController, hasFixedSize = false)
recyclerView.adapter = epoxyController.adapter
bottomSheetTitle.text = context?.getString(R.string.reactions) bottomSheetTitle.text = context?.getString(R.string.reactions)
} }
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
epoxyController.setData(it) epoxyController.setData(it)
super.invalidate() super.invalidate()

View file

@ -36,9 +36,7 @@ class FilteredRoomsActivity : VectorBaseActivity() {
return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomListFragment return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomListFragment
} }
override fun getLayoutRes(): Int { override fun getLayoutRes() = R.layout.activity_filtered_rooms
return R.layout.activity_filtered_rooms
}
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)

View file

@ -26,6 +26,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.* import com.airbnb.mvrx.*
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import im.vector.matrix.android.api.failure.Failure 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.R
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.error.ErrorFormatter 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.OnBackPressed
import im.vector.riotx.core.platform.StateView import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.RoomListDisplayMode 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.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.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.riotx.features.home.room.list.widget.FabMenuView import im.vector.riotx.features.home.room.list.widget.FabMenuView
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
@ -65,6 +66,7 @@ class RoomListFragment @Inject constructor(
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener { ) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel
private val roomListParams: RoomListParams by args() private val roomListParams: RoomListParams by args()
private val roomListViewModel: RoomListViewModel by fragmentViewModel() private val roomListViewModel: RoomListViewModel by fragmentViewModel()
@ -118,8 +120,12 @@ class RoomListFragment @Inject constructor(
} }
override fun onDestroyView() { override fun onDestroyView() {
roomController.removeModelBuildListener(modelBuildListener)
modelBuildListener = null
roomListView.cleanup()
roomController.listener = null
createChatFabMenu.listener = null
super.onDestroyView() super.onDestroyView()
roomListView.adapter = null
} }
private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) { private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) {
@ -198,7 +204,8 @@ class RoomListFragment @Inject constructor(
roomListView.layoutManager = layoutManager roomListView.layoutManager = layoutManager
roomListView.itemAnimator = RoomListAnimator() roomListView.itemAnimator = RoomListAnimator()
roomController.listener = this roomController.listener = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) } modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) }
roomController.addModelBuildListener(modelBuildListener)
roomListView.adapter = roomController.adapter roomListView.adapter = roomController.adapter
stateView.contentView = roomListView stateView.contentView = roomListView
} }

View file

@ -123,7 +123,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
) )
} }
session.getRoom(roomId)?.join(emptyList(), object : MatrixCallback<Unit> { session.getRoom(roomId)?.join(callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data. // 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 // 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<Unit> { session.getRoom(roomId)?.leave(null, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data. // 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 // 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) { private fun handleLeaveRoom(action: RoomListAction.LeaveRoom) {
session.getRoom(action.roomId)?.leave(object : MatrixCallback<Unit> { session.getRoom(action.roomId)?.leave(null, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
_viewEvents.post(RoomListViewEvents.Failure(failure)) _viewEvents.post(RoomListViewEvents.Failure(failure))
} }

View file

@ -42,7 +42,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
init { init {
// We are requesting a model build directly as the first build of epoxy is on the main thread. // 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() requestModelBuild()
} }

View file

@ -21,7 +21,6 @@ import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import butterknife.ButterKnife import butterknife.ButterKnife
@ -29,6 +28,8 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent 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.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@ -56,8 +57,8 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R
override val showExpanded = true override val showExpanded = true
override fun injectWith(screenComponent: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
screenComponent.inject(this) injector.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -69,13 +70,17 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false) recyclerView.configureWith(roomListActionsEpoxyController, hasFixedSize = false)
recyclerView.adapter = roomListActionsEpoxyController.adapter
// Disable item animation // Disable item animation
recyclerView.itemAnimator = null recyclerView.itemAnimator = null
roomListActionsEpoxyController.listener = this roomListActionsEpoxyController.listener = this
} }
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { override fun invalidate() = withState(viewModel) {
roomListActionsEpoxyController.setData(it) roomListActionsEpoxyController.setData(it)
super.invalidate() super.invalidate()

View file

@ -539,7 +539,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
setState { setState {
copy( copy(
asyncHomeServerLoginFlowRequest = Uninitialized, asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = action.homeServerUrl, homeServerUrl = data.homeServerUrl,
loginMode = loginMode, loginMode = loginMode,
loginModeSupportedTypes = data.loginFlowResponse.flows.mapNotNull { it.type }.toList() loginModeSupportedTypes = data.loginFlowResponse.flows.mapNotNull { it.type }.toList()
) )

View file

@ -24,6 +24,8 @@ import butterknife.OnClick
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter 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.core.utils.openUrlInExternalBrowser
import im.vector.riotx.features.login.AbstractLoginFragment import im.vector.riotx.features.login.AbstractLoginFragment
import im.vector.riotx.features.login.LoginAction import im.vector.riotx.features.login.LoginAction
@ -55,8 +57,7 @@ class LoginTermsFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
loginTermsPolicyList.configureWith(policyController)
loginTermsPolicyList.setController(policyController)
policyController.listener = this policyController.listener = this
val list = ArrayList<LocalizedFlowDataLoginTermsChecked>() val list = ArrayList<LocalizedFlowDataLoginTermsChecked>()
@ -69,6 +70,12 @@ class LoginTermsFragment @Inject constructor(
loginTermsViewState = LoginTermsViewState(list) loginTermsViewState = LoginTermsViewState(list)
} }
override fun onDestroyView() {
loginTermsPolicyList.cleanup()
policyController.listener = null
super.onDestroyView()
}
private fun renderState() { private fun renderState() {
policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked) policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked)

View file

@ -74,14 +74,14 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
private fun handleJoinRoom(roomId: String) { private fun handleJoinRoom(roomId: String) {
activeSessionHolder.getSafeActiveSession()?.let { session -> activeSessionHolder.getSafeActiveSession()?.let { session ->
session.getRoom(roomId) session.getRoom(roomId)
?.join(emptyList(), object : MatrixCallback<Unit> {}) ?.join(callback = object : MatrixCallback<Unit> {})
} }
} }
private fun handleRejectRoom(roomId: String) { private fun handleRejectRoom(roomId: String) {
activeSessionHolder.getSafeActiveSession()?.let { session -> activeSessionHolder.getSafeActiveSession()?.let { session ->
session.getRoom(roomId) session.getRoom(roomId)
?.leave(object : MatrixCallback<Unit> {}) ?.leave(callback = object : MatrixCallback<Unit> {})
} }
} }

View file

@ -17,12 +17,18 @@ package im.vector.riotx.features.reactions
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.lifecycle.observe
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.emoji_chooser_fragment.*
import javax.inject.Inject 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 override fun getLayoutResId() = R.layout.emoji_chooser_fragment
@ -31,10 +37,29 @@ class EmojiChooserFragment @Inject constructor() : VectorBaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java) viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java)
viewModel.initWithContext(context!!)
(view as? RecyclerView)?.let { emojiRecyclerAdapter.reactionClickListener = this
it.adapter = viewModel.adapter emojiRecyclerAdapter.interactionListener = this
it.adapter?.notifyDataSetChanged()
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()
}
} }

View file

@ -15,7 +15,6 @@
*/ */
package im.vector.riotx.features.reactions package im.vector.riotx.features.reactions
import android.content.Context
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
@ -23,36 +22,26 @@ import javax.inject.Inject
class EmojiChooserViewModel @Inject constructor() : ViewModel() { class EmojiChooserViewModel @Inject constructor() : ViewModel() {
var adapter: EmojiRecyclerAdapter? = null
val emojiSourceLiveData: MutableLiveData<EmojiDataSource> = MutableLiveData()
val navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData() val navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
var selectedReaction: String? = null var selectedReaction: String? = null
var eventId: String? = null var eventId: String? = null
val currentSection: MutableLiveData<Int> = MutableLiveData() val currentSection: MutableLiveData<Int> = MutableLiveData()
val moveToSection: MutableLiveData<Int> = MutableLiveData()
var reactionClickListener = object : ReactionClickListener { fun onReactionSelected(reaction: String) {
override fun onReactionSelected(reaction: String) { selectedReaction = reaction
selectedReaction = reaction navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
}
} }
fun initWithContext(context: Context) { // Called by the Fragment, when the List is scrolled
// TODO load async fun setCurrentSection(section: Int) {
val emojiDataSource = EmojiDataSource(context) currentSection.value = section
emojiSourceLiveData.value = emojiDataSource
adapter = EmojiRecyclerAdapter(emojiDataSource, reactionClickListener)
adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener {
override fun firstVisibleSectionChange(section: Int) {
currentSection.value = section
}
}
} }
fun scrollToSection(sectionIndex: Int) { // Called by the Activity, when a tab item is clicked
adapter?.scrollToSection(sectionIndex) fun scrollToSection(section: Int) {
moveToSection.value = section
} }
companion object { companion object {

View file

@ -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<EmojiCategory>,
val emojis: Map<String, EmojiItem>,
val aliases: Map<String, String>)
@JsonClass(generateAdapter = true)
data class EmojiCategory(val id: String, val name: String, val emojis: List<String>)
@JsonClass(generateAdapter = true)
data class EmojiItem(
@Json(name = "a") val name: String,
@Json(name = "b") val unicode: String,
@Json(name = "j") val keywords: List<String>?,
val k: List<String>?) {
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',
}

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