Merge branch 'develop' into feature/sync_issues
This commit is contained in:
		
						commit
						be2e5117ca
					
				
							
								
								
									
										10
									
								
								CHANGES.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								CHANGES.md
									
									
									
									
									
								
							@ -2,16 +2,18 @@ Changes in RiotX 0.10.0 (2019-XX-XX)
 | 
			
		||||
===================================================
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
Translations 🗣:
 | 
			
		||||
 -
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
@ -57,8 +57,9 @@ class RxRoom(private val room: Room) {
 | 
			
		||||
        room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun joinRoom(viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
 | 
			
		||||
        room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it)
 | 
			
		||||
    fun joinRoom(reason: String? = null,
 | 
			
		||||
                 viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
 | 
			
		||||
        room.join(reason, viaServers, MatrixCallbackSingle(it)).toSingle(it)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,10 @@ class RxSession(private val session: Session) {
 | 
			
		||||
        return session.liveGroupSummaries().asObservable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
 | 
			
		||||
        return session.liveBreadcrumbs().asObservable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun liveSyncState(): Observable<SyncState> {
 | 
			
		||||
        return session.syncState().asObservable()
 | 
			
		||||
    }
 | 
			
		||||
@ -72,8 +76,10 @@ class RxSession(private val session: Session) {
 | 
			
		||||
        session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun joinRoom(roomId: String, viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
 | 
			
		||||
        session.joinRoom(roomId, viaServers, MatrixCallbackSingle(it)).toSingle(it)
 | 
			
		||||
    fun joinRoom(roomId: String,
 | 
			
		||||
                 reason: String? = null,
 | 
			
		||||
                 viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
 | 
			
		||||
        session.joinRoom(roomId, reason, viaServers, MatrixCallbackSingle(it)).toSingle(it)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,8 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
 | 
			
		||||
sealed class LoginFlowResult {
 | 
			
		||||
    data class Success(
 | 
			
		||||
            val loginFlowResponse: LoginFlowResponse,
 | 
			
		||||
            val isLoginAndRegistrationSupported: Boolean
 | 
			
		||||
            val isLoginAndRegistrationSupported: Boolean,
 | 
			
		||||
            val homeServerUrl: String
 | 
			
		||||
    ) : LoginFlowResult()
 | 
			
		||||
 | 
			
		||||
    object OutdatedHomeserver : LoginFlowResult()
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
 | 
			
		||||
    data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
 | 
			
		||||
    object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
 | 
			
		||||
    // When server send an error, but it cannot be interpreted as a MatrixError
 | 
			
		||||
    data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody))
 | 
			
		||||
    data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody"))
 | 
			
		||||
 | 
			
		||||
    data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString()))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,8 @@ interface ContentUploadStateTracker {
 | 
			
		||||
 | 
			
		||||
    fun untrack(key: String, updateListener: UpdateListener)
 | 
			
		||||
 | 
			
		||||
    fun clear()
 | 
			
		||||
 | 
			
		||||
    interface UpdateListener {
 | 
			
		||||
        fun onUpdate(state: State)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -30,12 +30,16 @@ interface RoomDirectoryService {
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
     */
 | 
			
		||||
    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.
 | 
			
		||||
 | 
			
		||||
@ -30,14 +30,17 @@ interface RoomService {
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
     * @param roomId the roomId of the room to join
 | 
			
		||||
     * @param reason optional reason for joining the room
 | 
			
		||||
     * @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room.
 | 
			
		||||
     */
 | 
			
		||||
    fun joinRoom(roomId: String,
 | 
			
		||||
                 reason: String? = null,
 | 
			
		||||
                 viaServers: List<String> = emptyList(),
 | 
			
		||||
                 callback: MatrixCallback<Unit>): Cancelable
 | 
			
		||||
 | 
			
		||||
@ -54,8 +57,21 @@ interface RoomService {
 | 
			
		||||
     */
 | 
			
		||||
    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
 | 
			
		||||
     */
 | 
			
		||||
    fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable
 | 
			
		||||
    fun markAllAsRead(roomIds: List<String>,
 | 
			
		||||
                      callback: MatrixCallback<Unit>): Cancelable
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -52,16 +52,21 @@ interface MembershipService {
 | 
			
		||||
    /**
 | 
			
		||||
     * 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.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
    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.
 | 
			
		||||
     */
 | 
			
		||||
    fun leave(callback: MatrixCallback<Unit>): Cancelable
 | 
			
		||||
    fun leave(reason: String? = null,
 | 
			
		||||
              callback: MatrixCallback<Unit>): Cancelable
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -26,9 +26,13 @@ import im.vector.matrix.android.api.session.events.model.UnsignedData
 | 
			
		||||
@JsonClass(generateAdapter = true)
 | 
			
		||||
data class RoomMember(
 | 
			
		||||
        @Json(name = "membership") val membership: Membership,
 | 
			
		||||
        @Json(name = "reason") val reason: String? = null,
 | 
			
		||||
        @Json(name = "displayname") val displayName: String? = null,
 | 
			
		||||
        @Json(name = "avatar_url") val avatarUrl: String? = null,
 | 
			
		||||
        @Json(name = "is_direct") val isDirect: Boolean = false,
 | 
			
		||||
        @Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null,
 | 
			
		||||
        @Json(name = "unsigned") val unsignedData: UnsignedData? = null
 | 
			
		||||
)
 | 
			
		||||
) {
 | 
			
		||||
    val safeReason
 | 
			
		||||
        get() = reason?.takeIf { it.isNotBlank() }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -50,6 +50,7 @@ interface RelationService {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sends a reaction (emoji) to the targetedEvent.
 | 
			
		||||
     * It has no effect if the user has already added the same reaction to the event.
 | 
			
		||||
     * @param targetEventId the id of the event being reacted
 | 
			
		||||
     * @param reaction the reaction (preferably emoji)
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ import im.vector.matrix.android.api.auth.data.Credentials
 | 
			
		||||
import im.vector.matrix.android.api.auth.data.Versions
 | 
			
		||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
 | 
			
		||||
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
 | 
			
		||||
import im.vector.matrix.android.internal.auth.data.RiotConfig
 | 
			
		||||
import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed
 | 
			
		||||
import im.vector.matrix.android.internal.auth.registration.*
 | 
			
		||||
import im.vector.matrix.android.internal.network.NetworkConstants
 | 
			
		||||
@ -31,6 +32,12 @@ import retrofit2.http.*
 | 
			
		||||
 */
 | 
			
		||||
internal interface AuthAPI {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a Riot config file
 | 
			
		||||
     */
 | 
			
		||||
    @GET("config.json")
 | 
			
		||||
    fun getRiotConfig(): Call<RiotConfig>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the version information of the homeserver
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -16,16 +16,19 @@
 | 
			
		||||
 | 
			
		||||
package im.vector.matrix.android.internal.auth
 | 
			
		||||
 | 
			
		||||
import android.net.Uri
 | 
			
		||||
import dagger.Lazy
 | 
			
		||||
import im.vector.matrix.android.api.MatrixCallback
 | 
			
		||||
import im.vector.matrix.android.api.auth.AuthenticationService
 | 
			
		||||
import im.vector.matrix.android.api.auth.data.*
 | 
			
		||||
import im.vector.matrix.android.api.auth.login.LoginWizard
 | 
			
		||||
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
 | 
			
		||||
import im.vector.matrix.android.api.failure.Failure
 | 
			
		||||
import im.vector.matrix.android.api.session.Session
 | 
			
		||||
import im.vector.matrix.android.api.util.Cancelable
 | 
			
		||||
import im.vector.matrix.android.internal.SessionManager
 | 
			
		||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
 | 
			
		||||
import im.vector.matrix.android.internal.auth.data.RiotConfig
 | 
			
		||||
import im.vector.matrix.android.internal.auth.db.PendingSessionData
 | 
			
		||||
import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard
 | 
			
		||||
import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard
 | 
			
		||||
@ -40,6 +43,7 @@ import kotlinx.coroutines.launch
 | 
			
		||||
import kotlinx.coroutines.withContext
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
import javax.net.ssl.HttpsURLConnection
 | 
			
		||||
 | 
			
		||||
internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
 | 
			
		||||
                                                                private val okHttpClient: Lazy<OkHttpClient>,
 | 
			
		||||
@ -84,7 +88,12 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
 | 
			
		||||
                    {
 | 
			
		||||
                        if (it is LoginFlowResult.Success) {
 | 
			
		||||
                            // The homeserver exists and up to date, keep the config
 | 
			
		||||
                            pendingSessionData = PendingSessionData(homeServerConnectionConfig)
 | 
			
		||||
                            // Homeserver url may have been changed, if it was a Riot url
 | 
			
		||||
                            val alteredHomeServerConnectionConfig = homeServerConnectionConfig.copy(
 | 
			
		||||
                                    homeServerUri = Uri.parse(it.homeServerUrl)
 | 
			
		||||
                            )
 | 
			
		||||
 | 
			
		||||
                            pendingSessionData = PendingSessionData(alteredHomeServerConnectionConfig)
 | 
			
		||||
                                    .also { data -> pendingSessionStore.savePendingSessionData(data) }
 | 
			
		||||
                        }
 | 
			
		||||
                        callback.onSuccess(it)
 | 
			
		||||
@ -97,20 +106,71 @@ internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
 | 
			
		||||
                .toCancelable()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
 | 
			
		||||
    private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult {
 | 
			
		||||
        return withContext(coroutineDispatchers.io) {
 | 
			
		||||
            val authAPI = buildAuthAPI(homeServerConnectionConfig)
 | 
			
		||||
 | 
			
		||||
            // First check the homeserver version
 | 
			
		||||
            runCatching {
 | 
			
		||||
                executeRequest<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)
 | 
			
		||||
 | 
			
		||||
        // First check the homeserver version
 | 
			
		||||
        val versions = executeRequest<Versions> {
 | 
			
		||||
            apiCall = authAPI.versions()
 | 
			
		||||
        // Ok, try to get the config.json file of a RiotWeb client
 | 
			
		||||
        val riotConfig = executeRequest<RiotConfig> {
 | 
			
		||||
            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
 | 
			
		||||
            val loginFlowResponse = executeRequest<LoginFlowResponse> {
 | 
			
		||||
                apiCall = authAPI.getLoginFlows()
 | 
			
		||||
            }
 | 
			
		||||
            LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk())
 | 
			
		||||
            LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
 | 
			
		||||
        } else {
 | 
			
		||||
            // Not supported
 | 
			
		||||
            LoginFlowResult.OutdatedHomeserver
 | 
			
		||||
 | 
			
		||||
@ -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?
 | 
			
		||||
)
 | 
			
		||||
@ -19,16 +19,14 @@ package im.vector.matrix.android.internal.crypto.store.db.query
 | 
			
		||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity
 | 
			
		||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields
 | 
			
		||||
import io.realm.Realm
 | 
			
		||||
import io.realm.kotlin.createObject
 | 
			
		||||
import io.realm.kotlin.where
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get or create a room
 | 
			
		||||
 */
 | 
			
		||||
internal fun CryptoRoomEntity.Companion.getOrCreate(realm: Realm, roomId: String): CryptoRoomEntity {
 | 
			
		||||
    return getById(realm, roomId)
 | 
			
		||||
            ?: let {
 | 
			
		||||
                realm.createObject(CryptoRoomEntity::class.java, roomId)
 | 
			
		||||
            }
 | 
			
		||||
    return getById(realm, roomId) ?: realm.createObject(roomId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -20,18 +20,20 @@ import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
 | 
			
		||||
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
 | 
			
		||||
import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey
 | 
			
		||||
import io.realm.Realm
 | 
			
		||||
import io.realm.kotlin.createObject
 | 
			
		||||
import io.realm.kotlin.where
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get or create a device info
 | 
			
		||||
 */
 | 
			
		||||
internal fun DeviceInfoEntity.Companion.getOrCreate(realm: Realm, userId: String, deviceId: String): DeviceInfoEntity {
 | 
			
		||||
    val key = DeviceInfoEntity.createPrimaryKey(userId, deviceId)
 | 
			
		||||
 | 
			
		||||
    return realm.where<DeviceInfoEntity>()
 | 
			
		||||
            .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId))
 | 
			
		||||
            .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, key)
 | 
			
		||||
            .findFirst()
 | 
			
		||||
            ?: let {
 | 
			
		||||
                realm.createObject(DeviceInfoEntity::class.java, DeviceInfoEntity.createPrimaryKey(userId, deviceId)).apply {
 | 
			
		||||
                    this.deviceId = deviceId
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            ?: realm.createObject<DeviceInfoEntity>(key)
 | 
			
		||||
                    .apply {
 | 
			
		||||
                        this.deviceId = deviceId
 | 
			
		||||
                    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.store.db.query
 | 
			
		||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
 | 
			
		||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
 | 
			
		||||
import io.realm.Realm
 | 
			
		||||
import io.realm.kotlin.createObject
 | 
			
		||||
import io.realm.kotlin.where
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -28,9 +29,7 @@ internal fun UserEntity.Companion.getOrCreate(realm: Realm, userId: String): Use
 | 
			
		||||
    return realm.where<UserEntity>()
 | 
			
		||||
            .equalTo(UserEntityFields.USER_ID, userId)
 | 
			
		||||
            .findFirst()
 | 
			
		||||
            ?: let {
 | 
			
		||||
                realm.createObject(UserEntity::class.java, userId)
 | 
			
		||||
            }
 | 
			
		||||
            ?: realm.createObject(userId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
@ -38,7 +38,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
 | 
			
		||||
                                      var readMarkerId: String? = null,
 | 
			
		||||
                                      var hasUnreadMessages: Boolean = false,
 | 
			
		||||
                                      var tags: RealmList<RoomTagEntity> = RealmList(),
 | 
			
		||||
                                      var userDrafts: UserDraftsEntity? = null
 | 
			
		||||
                                      var userDrafts: UserDraftsEntity? = null,
 | 
			
		||||
                                      var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS
 | 
			
		||||
) : RealmObject() {
 | 
			
		||||
 | 
			
		||||
    private var membershipStr: String = Membership.NONE.name
 | 
			
		||||
@ -59,5 +60,7 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
 | 
			
		||||
            versioningStateStr = value.name
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    companion object
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val NOT_IN_BREADCRUMBS = -1
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule
 | 
			
		||||
                 SyncEntity::class,
 | 
			
		||||
                 UserEntity::class,
 | 
			
		||||
                 IgnoredUserEntity::class,
 | 
			
		||||
                 BreadcrumbsEntity::class,
 | 
			
		||||
                 EventAnnotationsSummaryEntity::class,
 | 
			
		||||
                 ReactionAggregatedSummaryEntity::class,
 | 
			
		||||
                 EditAggregatedSummaryEntity::class,
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
}
 | 
			
		||||
@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
 | 
			
		||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
 | 
			
		||||
import io.realm.Realm
 | 
			
		||||
import io.realm.RealmQuery
 | 
			
		||||
import io.realm.kotlin.createObject
 | 
			
		||||
import io.realm.kotlin.where
 | 
			
		||||
 | 
			
		||||
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<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 {
 | 
			
		||||
    return where(realm, roomId).findFirst()
 | 
			
		||||
           ?: realm.createObject(ReadMarkerEntity::class.java, roomId)
 | 
			
		||||
    return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
 | 
			
		||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields
 | 
			
		||||
import io.realm.Realm
 | 
			
		||||
import io.realm.RealmQuery
 | 
			
		||||
import io.realm.kotlin.createObject
 | 
			
		||||
import io.realm.kotlin.where
 | 
			
		||||
 | 
			
		||||
internal fun ReadReceiptEntity.Companion.where(realm: Realm, roomId: String, userId: String): RealmQuery<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 {
 | 
			
		||||
    return ReadReceiptEntity.where(realm, roomId, userId).findFirst()
 | 
			
		||||
           ?: realm.createObject(ReadReceiptEntity::class.java, buildPrimaryKey(roomId, userId)).apply {
 | 
			
		||||
               this.roomId = roomId
 | 
			
		||||
               this.userId = userId
 | 
			
		||||
           }
 | 
			
		||||
            ?: realm.createObject<ReadReceiptEntity>(buildPrimaryKey(roomId, userId))
 | 
			
		||||
                    .apply {
 | 
			
		||||
                        this.roomId = roomId
 | 
			
		||||
                        this.userId = userId
 | 
			
		||||
                    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun buildPrimaryKey(roomId: String, userId: String) = "${roomId}_$userId"
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
 | 
			
		||||
import io.realm.Realm
 | 
			
		||||
import io.realm.RealmQuery
 | 
			
		||||
import io.realm.RealmResults
 | 
			
		||||
import io.realm.kotlin.createObject
 | 
			
		||||
import io.realm.kotlin.where
 | 
			
		||||
 | 
			
		||||
internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<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 {
 | 
			
		||||
    return where(realm, roomId).findFirst()
 | 
			
		||||
           ?: realm.createObject(RoomSummaryEntity::class.java, roomId)
 | 
			
		||||
    return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm): RealmResults<RoomSummaryEntity> {
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.model.message.*
 | 
			
		||||
import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory
 | 
			
		||||
import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter
 | 
			
		||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
 | 
			
		||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataBreadcrumbs
 | 
			
		||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages
 | 
			
		||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback
 | 
			
		||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers
 | 
			
		||||
@ -34,6 +35,7 @@ object MoshiProvider {
 | 
			
		||||
                    .registerSubtype(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES)
 | 
			
		||||
                    .registerSubtype(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST)
 | 
			
		||||
                    .registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES)
 | 
			
		||||
                    .registerSubtype(UserAccountDataBreadcrumbs::class.java, UserAccountData.TYPE_BREADCRUMBS)
 | 
			
		||||
            )
 | 
			
		||||
            .add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java)
 | 
			
		||||
                    .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT)
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@
 | 
			
		||||
package im.vector.matrix.android.internal.network
 | 
			
		||||
 | 
			
		||||
import com.squareup.moshi.JsonDataException
 | 
			
		||||
import com.squareup.moshi.JsonEncodingException
 | 
			
		||||
import im.vector.matrix.android.api.failure.ConsentNotGivenError
 | 
			
		||||
import im.vector.matrix.android.api.failure.Failure
 | 
			
		||||
import im.vector.matrix.android.api.failure.MatrixError
 | 
			
		||||
@ -106,6 +107,9 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
 | 
			
		||||
    } catch (ex: JsonDataException) {
 | 
			
		||||
        // This is not a MatrixError
 | 
			
		||||
        Timber.w("The error returned by the server is not a MatrixError")
 | 
			
		||||
    } catch (ex: JsonEncodingException) {
 | 
			
		||||
        // This is not a MatrixError, HTML code?
 | 
			
		||||
        Timber.w("The error returned by the server is not a MatrixError, probably HTML string")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Failure.OtherServerError(errorBodyStr, httpCode)
 | 
			
		||||
 | 
			
		||||
@ -42,6 +42,10 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun clear() {
 | 
			
		||||
        listeners.clear()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    internal fun setFailure(key: String, throwable: Throwable) {
 | 
			
		||||
        val failure = ContentUploadStateTracker.State.Failure(throwable)
 | 
			
		||||
        updateState(key, failure)
 | 
			
		||||
 | 
			
		||||
@ -44,9 +44,9 @@ internal class DefaultRoomDirectoryService @Inject constructor(private val getPu
 | 
			
		||||
                .executeBy(taskExecutor)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable {
 | 
			
		||||
    override fun joinRoom(roomId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
 | 
			
		||||
        return joinRoomTask
 | 
			
		||||
                .configureWith(JoinRoomTask.Params(roomId)) {
 | 
			
		||||
                .configureWith(JoinRoomTask.Params(roomId, reason)) {
 | 
			
		||||
                    this.callback = callback
 | 
			
		||||
                }
 | 
			
		||||
                .executeBy(taskExecutor)
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,7 @@ import im.vector.matrix.android.internal.database.query.where
 | 
			
		||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
 | 
			
		||||
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
 | 
			
		||||
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
 | 
			
		||||
import im.vector.matrix.android.internal.session.user.accountdata.UpdateBreadcrumbsTask
 | 
			
		||||
import im.vector.matrix.android.internal.task.TaskExecutor
 | 
			
		||||
import im.vector.matrix.android.internal.task.configureWith
 | 
			
		||||
import io.realm.Realm
 | 
			
		||||
@ -43,6 +44,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
 | 
			
		||||
                                                      private val createRoomTask: CreateRoomTask,
 | 
			
		||||
                                                      private val joinRoomTask: JoinRoomTask,
 | 
			
		||||
                                                      private val markAllRoomsReadTask: MarkAllRoomsReadTask,
 | 
			
		||||
                                                      private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
 | 
			
		||||
                                                      private val roomFactory: RoomFactory,
 | 
			
		||||
                                                      private val taskExecutor: TaskExecutor) : RoomService {
 | 
			
		||||
 | 
			
		||||
@ -75,9 +77,28 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun joinRoom(roomId: String, viaServers: List<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
 | 
			
		||||
                .configureWith(JoinRoomTask.Params(roomId, viaServers)) {
 | 
			
		||||
                .configureWith(JoinRoomTask.Params(roomId, reason, viaServers)) {
 | 
			
		||||
                    this.callback = callback
 | 
			
		||||
                }
 | 
			
		||||
                .executeBy(taskExecutor)
 | 
			
		||||
 | 
			
		||||
@ -217,7 +217,7 @@ internal interface RoomAPI {
 | 
			
		||||
    @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join")
 | 
			
		||||
    fun join(@Path("roomId") roomId: String,
 | 
			
		||||
             @Query("server_name") viaServers: List<String>,
 | 
			
		||||
             @Body params: Map<String, String>): Call<Unit>
 | 
			
		||||
             @Body params: Map<String, String?>): Call<Unit>
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Leave the given room.
 | 
			
		||||
@ -227,7 +227,7 @@ internal interface RoomAPI {
 | 
			
		||||
     */
 | 
			
		||||
    @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave")
 | 
			
		||||
    fun leave(@Path("roomId") roomId: String,
 | 
			
		||||
              @Body params: Map<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.
 | 
			
		||||
 | 
			
		||||
@ -83,8 +83,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
 | 
			
		||||
        return result
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable {
 | 
			
		||||
        val params = InviteTask.Params(roomId, userId)
 | 
			
		||||
    override fun invite(userId: String, reason: String?, callback: MatrixCallback<Unit>): Cancelable {
 | 
			
		||||
        val params = InviteTask.Params(roomId, userId, reason)
 | 
			
		||||
        return inviteTask
 | 
			
		||||
                .configureWith(params) {
 | 
			
		||||
                    this.callback = callback
 | 
			
		||||
@ -92,8 +92,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
 | 
			
		||||
                .executeBy(taskExecutor)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun join(viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
 | 
			
		||||
        val params = JoinRoomTask.Params(roomId, viaServers)
 | 
			
		||||
    override fun join(reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
 | 
			
		||||
        val params = JoinRoomTask.Params(roomId, reason, viaServers)
 | 
			
		||||
        return joinTask
 | 
			
		||||
                .configureWith(params) {
 | 
			
		||||
                    this.callback = callback
 | 
			
		||||
@ -101,8 +101,8 @@ internal class DefaultMembershipService @AssistedInject constructor(@Assisted pr
 | 
			
		||||
                .executeBy(taskExecutor)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun leave(callback: MatrixCallback<Unit>): Cancelable {
 | 
			
		||||
        val params = LeaveRoomTask.Params(roomId)
 | 
			
		||||
    override fun leave(reason: String?, callback: MatrixCallback<Unit>): Cancelable {
 | 
			
		||||
        val params = LeaveRoomTask.Params(roomId, reason)
 | 
			
		||||
        return leaveRoomTask
 | 
			
		||||
                .configureWith(params) {
 | 
			
		||||
                    this.callback = callback
 | 
			
		||||
 | 
			
		||||
@ -21,5 +21,6 @@ import com.squareup.moshi.JsonClass
 | 
			
		||||
 | 
			
		||||
@JsonClass(generateAdapter = true)
 | 
			
		||||
data class InviteBody(
 | 
			
		||||
        @Json(name = "user_id") val userId: String
 | 
			
		||||
        @Json(name = "user_id") val userId: String,
 | 
			
		||||
        @Json(name = "reason") val reason: String?
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -24,7 +24,8 @@ import javax.inject.Inject
 | 
			
		||||
internal interface InviteTask : Task<InviteTask.Params, Unit> {
 | 
			
		||||
    data class Params(
 | 
			
		||||
            val roomId: String,
 | 
			
		||||
            val userId: String
 | 
			
		||||
            val userId: String,
 | 
			
		||||
            val reason: String?
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -32,7 +33,7 @@ internal class DefaultInviteTask @Inject constructor(private val roomAPI: RoomAP
 | 
			
		||||
 | 
			
		||||
    override suspend fun execute(params: InviteTask.Params) {
 | 
			
		||||
        return executeRequest {
 | 
			
		||||
            val body = InviteBody(params.userId)
 | 
			
		||||
            val body = InviteBody(params.userId, params.reason)
 | 
			
		||||
            apiCall = roomAPI.invite(params.roomId, body)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,7 @@ import javax.inject.Inject
 | 
			
		||||
internal interface JoinRoomTask : Task<JoinRoomTask.Params, Unit> {
 | 
			
		||||
    data class Params(
 | 
			
		||||
            val roomId: String,
 | 
			
		||||
            val reason: String?,
 | 
			
		||||
            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) {
 | 
			
		||||
        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
 | 
			
		||||
        // Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,8 @@ import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
internal interface LeaveRoomTask : Task<LeaveRoomTask.Params, Unit> {
 | 
			
		||||
    data class Params(
 | 
			
		||||
            val roomId: String
 | 
			
		||||
            val roomId: String,
 | 
			
		||||
            val reason: String?
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -31,7 +32,7 @@ internal class DefaultLeaveRoomTask @Inject constructor(private val roomAPI: Roo
 | 
			
		||||
 | 
			
		||||
    override suspend fun execute(params: LeaveRoomTask.Params) {
 | 
			
		||||
        return executeRequest {
 | 
			
		||||
            apiCall = roomAPI.leave(params.roomId, HashMap())
 | 
			
		||||
            apiCall = roomAPI.leave(params.roomId, mapOf("reason" to params.reason))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -30,10 +30,13 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
 | 
			
		||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 | 
			
		||||
import im.vector.matrix.android.api.util.Cancelable
 | 
			
		||||
import im.vector.matrix.android.api.util.NoOpCancellable
 | 
			
		||||
import im.vector.matrix.android.api.util.Optional
 | 
			
		||||
import im.vector.matrix.android.api.util.toOptional
 | 
			
		||||
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
 | 
			
		||||
import im.vector.matrix.android.internal.database.mapper.asDomain
 | 
			
		||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
 | 
			
		||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
 | 
			
		||||
import im.vector.matrix.android.internal.database.query.where
 | 
			
		||||
import im.vector.matrix.android.internal.di.UserId
 | 
			
		||||
import im.vector.matrix.android.internal.session.room.send.EncryptEventWorker
 | 
			
		||||
@ -44,6 +47,7 @@ import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEvent
 | 
			
		||||
import im.vector.matrix.android.internal.task.TaskExecutor
 | 
			
		||||
import im.vector.matrix.android.internal.task.configureWith
 | 
			
		||||
import im.vector.matrix.android.internal.util.CancelableWork
 | 
			
		||||
import im.vector.matrix.android.internal.util.fetchCopyMap
 | 
			
		||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
 | 
			
		||||
@ -54,6 +58,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
 | 
			
		||||
                                                                  private val cryptoService: CryptoService,
 | 
			
		||||
                                                                  private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
 | 
			
		||||
                                                                  private val fetchEditHistoryTask: FetchEditHistoryTask,
 | 
			
		||||
                                                                  private val timelineEventMapper: TimelineEventMapper,
 | 
			
		||||
                                                                  private val monarchy: Monarchy,
 | 
			
		||||
                                                                  private val taskExecutor: TaskExecutor)
 | 
			
		||||
    : RelationService {
 | 
			
		||||
@ -64,11 +69,27 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
 | 
			
		||||
        val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
 | 
			
		||||
                .also { saveLocalEcho(it) }
 | 
			
		||||
        val sendRelationWork = createSendEventWork(event, true)
 | 
			
		||||
        TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork)
 | 
			
		||||
        return CancelableWork(context, sendRelationWork.id)
 | 
			
		||||
        return if (monarchy
 | 
			
		||||
                        .fetchCopyMap(
 | 
			
		||||
                                { realm ->
 | 
			
		||||
                                    TimelineEventEntity.where(realm, roomId, targetEventId).findFirst()
 | 
			
		||||
                                },
 | 
			
		||||
                                { entity, _ ->
 | 
			
		||||
                                    timelineEventMapper.map(entity)
 | 
			
		||||
                                })
 | 
			
		||||
                        ?.annotations
 | 
			
		||||
                        ?.reactionsSummary
 | 
			
		||||
                        .orEmpty()
 | 
			
		||||
                        .none { it.addedByMe && it.key == reaction }) {
 | 
			
		||||
            val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
 | 
			
		||||
                    .also { saveLocalEcho(it) }
 | 
			
		||||
            val sendRelationWork = createSendEventWork(event, true)
 | 
			
		||||
            TimelineSendEventWorkCommon.postWork(context, roomId, sendRelationWork)
 | 
			
		||||
            CancelableWork(context, sendRelationWork.id)
 | 
			
		||||
        } else {
 | 
			
		||||
            Timber.w("Reaction already added")
 | 
			
		||||
            NoOpCancellable
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun undoReaction(targetEventId: String, reaction: String): Cancelable {
 | 
			
		||||
 | 
			
		||||
@ -78,7 +78,7 @@ internal class LocalEchoEventFactory @Inject constructor(
 | 
			
		||||
            val htmlText = renderer.render(document)
 | 
			
		||||
 | 
			
		||||
            if (isFormattedTextPertinent(source, htmlText)) {
 | 
			
		||||
                return TextContent(source, htmlText)
 | 
			
		||||
                return TextContent(text.toString(), htmlText)
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // Try to detect pills
 | 
			
		||||
 | 
			
		||||
@ -285,6 +285,9 @@ internal class DefaultTimeline(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
 | 
			
		||||
        if (listeners.contains(listener)) {
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
        listeners.add(listener).also {
 | 
			
		||||
            postSnapshot()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers
 | 
			
		||||
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
 | 
			
		||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.*
 | 
			
		||||
import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHelper
 | 
			
		||||
import im.vector.matrix.android.internal.session.user.accountdata.SaveBreadcrumbsTask
 | 
			
		||||
import im.vector.matrix.android.internal.session.user.accountdata.SaveIgnoredUsersTask
 | 
			
		||||
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
 | 
			
		||||
import im.vector.matrix.android.internal.task.TaskExecutor
 | 
			
		||||
@ -44,6 +45,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
 | 
			
		||||
                                                              private val updateUserAccountDataTask: UpdateUserAccountDataTask,
 | 
			
		||||
                                                              private val savePushRulesTask: SavePushRulesTask,
 | 
			
		||||
                                                              private val saveIgnoredUsersTask: SaveIgnoredUsersTask,
 | 
			
		||||
                                                              private val saveBreadcrumbsTask: SaveBreadcrumbsTask,
 | 
			
		||||
                                                              private val taskExecutor: TaskExecutor) {
 | 
			
		||||
 | 
			
		||||
    suspend fun handle(accountData: UserAccountDataSync?, invites: Map<String, InvitedRoomSync>?) {
 | 
			
		||||
@ -52,6 +54,7 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
 | 
			
		||||
                is UserAccountDataDirectMessages -> handleDirectChatRooms(it)
 | 
			
		||||
                is UserAccountDataPushRules      -> handlePushRules(it)
 | 
			
		||||
                is UserAccountDataIgnoredUsers   -> handleIgnoredUsers(it)
 | 
			
		||||
                is UserAccountDataBreadcrumbs    -> handleBreadcrumbs(it)
 | 
			
		||||
                is UserAccountDataFallback       -> Timber.d("Receive account data of unhandled type ${it.type}")
 | 
			
		||||
                else                             -> error("Missing code here!")
 | 
			
		||||
            }
 | 
			
		||||
@ -130,4 +133,10 @@ internal class UserAccountDataSyncHandler @Inject constructor(private val monarc
 | 
			
		||||
                .executeBy(taskExecutor)
 | 
			
		||||
        // TODO If not initial sync, we should execute a init sync
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleBreadcrumbs(userAccountDataBreadcrumbs: UserAccountDataBreadcrumbs) {
 | 
			
		||||
        saveBreadcrumbsTask
 | 
			
		||||
                .configureWith(SaveBreadcrumbsTask.Params(userAccountDataBreadcrumbs.content.recentRoomIds))
 | 
			
		||||
                .executeBy(taskExecutor)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ internal abstract class UserAccountData {
 | 
			
		||||
    companion object {
 | 
			
		||||
        const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list"
 | 
			
		||||
        const val TYPE_DIRECT_MESSAGES = "m.direct"
 | 
			
		||||
        const val TYPE_BREADCRUMBS = "im.vector.setting.breadcrumbs" // Was previously "im.vector.riot.breadcrumb_rooms"
 | 
			
		||||
        const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls"
 | 
			
		||||
        const val TYPE_WIDGETS = "m.widgets"
 | 
			
		||||
        const val TYPE_PUSH_RULES = "m.push_rules"
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
)
 | 
			
		||||
@ -35,5 +35,11 @@ internal abstract class AccountDataModule {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Binds
 | 
			
		||||
    abstract fun bindUpdateUserAccountDataTask(updateUserAccountDataTask: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask
 | 
			
		||||
    abstract fun bindUpdateUserAccountDataTask(task: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask
 | 
			
		||||
 | 
			
		||||
    @Binds
 | 
			
		||||
    abstract fun bindSaveBreadcrumbsTask(task: DefaultSaveBreadcrumbsTask): SaveBreadcrumbsTask
 | 
			
		||||
 | 
			
		||||
    @Binds
 | 
			
		||||
    abstract fun bindUpdateBreadcrumsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
        ))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.user.accountdata
 | 
			
		||||
 | 
			
		||||
import im.vector.matrix.android.internal.di.UserId
 | 
			
		||||
import im.vector.matrix.android.internal.network.executeRequest
 | 
			
		||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent
 | 
			
		||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
 | 
			
		||||
import im.vector.matrix.android.internal.task.Task
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
@ -38,6 +39,15 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
 | 
			
		||||
            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,
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,19 @@
 | 
			
		||||
<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>
 | 
			
		||||
</resources>
 | 
			
		||||
@ -81,4 +81,7 @@ layout_constraintLeft_
 | 
			
		||||
 | 
			
		||||
### Will crash on API < 21. Use ?colorAccent instead
 | 
			
		||||
\?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
 | 
			
		||||
 | 
			
		||||
@ -20,16 +20,22 @@ import android.os.Bundle
 | 
			
		||||
import androidx.appcompat.app.AppCompatActivity
 | 
			
		||||
import im.vector.matrix.android.api.crypto.getAllVerificationEmojis
 | 
			
		||||
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() {
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
        setContentView(R.layout.fragment_generic_recycler_epoxy)
 | 
			
		||||
 | 
			
		||||
        setContentView(R.layout.fragment_generic_recycler)
 | 
			
		||||
        val controller = SasEmojiController()
 | 
			
		||||
        epoxyRecyclerView.setController(controller)
 | 
			
		||||
        recyclerView.configureWith(controller)
 | 
			
		||||
        controller.setData(SasState(getAllVerificationEmojis()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroy() {
 | 
			
		||||
        recyclerView.cleanup()
 | 
			
		||||
        super.onDestroy()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -42,7 +42,7 @@ import im.vector.riotx.core.di.DaggerVectorComponent
 | 
			
		||||
import im.vector.riotx.core.di.HasVectorInjector
 | 
			
		||||
import im.vector.riotx.core.di.VectorComponent
 | 
			
		||||
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.lifecycle.VectorActivityLifecycleCallbacks
 | 
			
		||||
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 timber.log.Timber
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.Date
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
import java.util.*
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.Provider, androidx.work.Configuration.Provider {
 | 
			
		||||
@ -79,14 +78,13 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
 | 
			
		||||
    lateinit var vectorComponent: VectorComponent
 | 
			
		||||
    private var fontThreadHandler: Handler? = null
 | 
			
		||||
 | 
			
		||||
//    var slowMode = false
 | 
			
		||||
 | 
			
		||||
    override fun onCreate() {
 | 
			
		||||
        super.onCreate()
 | 
			
		||||
        appContext = this
 | 
			
		||||
        vectorComponent = DaggerVectorComponent.factory().create(this)
 | 
			
		||||
        vectorComponent.inject(this)
 | 
			
		||||
        vectorUncaughtExceptionHandler.activate(this)
 | 
			
		||||
        setupRxPlugin()
 | 
			
		||||
 | 
			
		||||
        if (BuildConfig.DEBUG) {
 | 
			
		||||
            Timber.plant(Timber.DebugTree())
 | 
			
		||||
@ -138,7 +136,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
 | 
			
		||||
        })
 | 
			
		||||
        ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
 | 
			
		||||
        // This should be done as early as possible
 | 
			
		||||
        initKnownEmojiHashSet(appContext)
 | 
			
		||||
        // initKnownEmojiHashSet(appContext)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION)
 | 
			
		||||
 | 
			
		||||
@ -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.CreateDirectRoomKnownUsersFragment
 | 
			
		||||
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.list.RoomListFragment
 | 
			
		||||
import im.vector.riotx.features.login.*
 | 
			
		||||
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.roomdirectory.PublicRoomsFragment
 | 
			
		||||
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
 | 
			
		||||
@ -249,4 +251,14 @@ interface FragmentModule {
 | 
			
		||||
    @IntoMap
 | 
			
		||||
    @FragmentKey(PublicRoomsFragment::class)
 | 
			
		||||
    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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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.home.HomeSharedActionViewModel
 | 
			
		||||
import im.vector.riotx.features.home.createdirect.CreateDirectRoomSharedActionViewModel
 | 
			
		||||
import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel
 | 
			
		||||
import im.vector.riotx.features.home.room.detail.timeline.action.MessageSharedActionViewModel
 | 
			
		||||
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
 | 
			
		||||
import im.vector.riotx.features.login.LoginSharedActionViewModel
 | 
			
		||||
@ -118,4 +119,9 @@ interface ViewModelModule {
 | 
			
		||||
    @IntoMap
 | 
			
		||||
    @ViewModelKey(LoginSharedActionViewModel::class)
 | 
			
		||||
    fun bindLoginSharedActionViewModel(viewModel: LoginSharedActionViewModel): ViewModel
 | 
			
		||||
 | 
			
		||||
    @Binds
 | 
			
		||||
    @IntoMap
 | 
			
		||||
    @ViewModelKey(RoomDetailSharedActionViewModel::class)
 | 
			
		||||
    fun bindRoomDetailSharedActionViewModel(viewModel: RoomDetailSharedActionViewModel): ViewModel
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ import im.vector.matrix.android.api.failure.Failure
 | 
			
		||||
import im.vector.matrix.android.api.failure.MatrixError
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.resources.StringProvider
 | 
			
		||||
import java.net.HttpURLConnection
 | 
			
		||||
import java.net.SocketTimeoutException
 | 
			
		||||
import java.net.UnknownHostException
 | 
			
		||||
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
 | 
			
		||||
        }
 | 
			
		||||
                ?: stringProvider.getString(R.string.unknown_error)
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
@ -51,7 +51,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        View.inflate(context, R.layout.view_state, this)
 | 
			
		||||
        layoutParams = LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
 | 
			
		||||
        layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
 | 
			
		||||
        errorRetryView.setOnClickListener {
 | 
			
		||||
            eventCallback?.onRetryClicked()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -73,7 +73,7 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
 | 
			
		||||
        injectWith(screenComponent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected open fun injectWith(screenComponent: ScreenComponent) = Unit
 | 
			
		||||
    protected open fun injectWith(injector: ScreenComponent) = Unit
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        mvrxViewIdProperty.restoreFrom(savedInstanceState)
 | 
			
		||||
 | 
			
		||||
@ -139,6 +139,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
 | 
			
		||||
    override fun onSaveInstanceState(outState: Bundle) {
 | 
			
		||||
        super.onSaveInstanceState(outState)
 | 
			
		||||
        restorables.forEach { it.onSaveInstanceState(outState) }
 | 
			
		||||
        restorables.clear()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewStateRestored(savedInstanceState: Bundle?) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										35
									
								
								vector/src/main/java/im/vector/riotx/core/rx/Rx.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								vector/src/main/java/im/vector/riotx/core/rx/Rx.kt
									
									
									
									
									
										Normal 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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -16,13 +16,6 @@
 | 
			
		||||
 | 
			
		||||
package im.vector.riotx.core.utils
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import com.squareup.moshi.Moshi
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.features.reactions.EmojiDataSource
 | 
			
		||||
import kotlinx.coroutines.GlobalScope
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import java.util.regex.Pattern
 | 
			
		||||
 | 
			
		||||
private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" +
 | 
			
		||||
@ -49,6 +42,7 @@ private val emojisPattern = Pattern.compile("((?:[\uD83C\uDF00-\uD83D\uDDFF]" +
 | 
			
		||||
        "|\uD83C\uDCCF\uFE0F?" +
 | 
			
		||||
        "|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?))")
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
// A hashset from all supported emoji
 | 
			
		||||
private var knownEmojiSet: HashSet<String>? = null
 | 
			
		||||
 | 
			
		||||
@ -56,7 +50,7 @@ fun initKnownEmojiHashSet(context: Context, done: (() -> Unit)? = null) {
 | 
			
		||||
    GlobalScope.launch {
 | 
			
		||||
        context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input ->
 | 
			
		||||
            val moshi = Moshi.Builder().build()
 | 
			
		||||
            val jsonAdapter = moshi.adapter(EmojiDataSource.EmojiData::class.java)
 | 
			
		||||
            val jsonAdapter = moshi.adapter(EmojiData::class.java)
 | 
			
		||||
            val inputAsString = input.bufferedReader().use { it.readText() }
 | 
			
		||||
            val source = jsonAdapter.fromJson(inputAsString)
 | 
			
		||||
            knownEmojiSet = HashSet<String>().also {
 | 
			
		||||
@ -77,6 +71,7 @@ fun isSingleEmoji(string: String): Boolean {
 | 
			
		||||
    }
 | 
			
		||||
    return knownEmojiSet?.contains(string) ?: false
 | 
			
		||||
}
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test if a string contains emojis.
 | 
			
		||||
 | 
			
		||||
@ -27,16 +27,19 @@ import im.vector.riotx.R
 | 
			
		||||
enum class Command(val command: String, val parameters: String, @StringRes val description: Int) {
 | 
			
		||||
    EMOTE("/me", "<message>", R.string.command_description_emote),
 | 
			
		||||
    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),
 | 
			
		||||
    RESET_USER_POWER_LEVEL("/deop", "<user-id>", R.string.command_description_deop_user),
 | 
			
		||||
    INVITE("/invite", "<user-id>", R.string.command_description_invite_user),
 | 
			
		||||
    JOIN_ROOM("/join", "<room-alias>", R.string.command_description_join_room),
 | 
			
		||||
    PART("/part", "<room-alias>", R.string.command_description_part_room),
 | 
			
		||||
    INVITE("/invite", "<user-id> [reason]", R.string.command_description_invite_user),
 | 
			
		||||
    JOIN_ROOM("/join", "<room-alias> [reason]", R.string.command_description_join_room),
 | 
			
		||||
    PART("/part", "<room-alias> [reason]", R.string.command_description_part_room),
 | 
			
		||||
    TOPIC("/topic", "<topic>", R.string.command_description_topic),
 | 
			
		||||
    KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user),
 | 
			
		||||
    CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick),
 | 
			
		||||
    MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown),
 | 
			
		||||
    CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token),
 | 
			
		||||
    SPOILER("/spoiler", "<message>", R.string.command_description_spoiler);
 | 
			
		||||
 | 
			
		||||
    val length
 | 
			
		||||
        get() = command.length + 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -81,29 +81,52 @@ object CommandParser {
 | 
			
		||||
                    ParsedCommand.SendEmote(message)
 | 
			
		||||
                }
 | 
			
		||||
                Command.JOIN_ROOM.command              -> {
 | 
			
		||||
                    val roomAlias = textMessage.substring(Command.JOIN_ROOM.command.length).trim()
 | 
			
		||||
                    if (messageParts.size >= 2) {
 | 
			
		||||
                        val roomAlias = messageParts[1]
 | 
			
		||||
 | 
			
		||||
                    if (roomAlias.isNotEmpty()) {
 | 
			
		||||
                        ParsedCommand.JoinRoom(roomAlias)
 | 
			
		||||
                        if (roomAlias.isNotEmpty()) {
 | 
			
		||||
                            ParsedCommand.JoinRoom(
 | 
			
		||||
                                    roomAlias,
 | 
			
		||||
                                    textMessage.substring(Command.JOIN_ROOM.length + roomAlias.length)
 | 
			
		||||
                                            .trim()
 | 
			
		||||
                                            .takeIf { it.isNotBlank() }
 | 
			
		||||
                            )
 | 
			
		||||
                        } else {
 | 
			
		||||
                            ParsedCommand.ErrorSyntax(Command.JOIN_ROOM)
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        ParsedCommand.ErrorSyntax(Command.JOIN_ROOM)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Command.PART.command                   -> {
 | 
			
		||||
                    val roomAlias = textMessage.substring(Command.PART.command.length).trim()
 | 
			
		||||
                    if (messageParts.size >= 2) {
 | 
			
		||||
                        val roomAlias = messageParts[1]
 | 
			
		||||
 | 
			
		||||
                    if (roomAlias.isNotEmpty()) {
 | 
			
		||||
                        ParsedCommand.PartRoom(roomAlias)
 | 
			
		||||
                        if (roomAlias.isNotEmpty()) {
 | 
			
		||||
                            ParsedCommand.PartRoom(
 | 
			
		||||
                                    roomAlias,
 | 
			
		||||
                                    textMessage.substring(Command.PART.length + roomAlias.length)
 | 
			
		||||
                                            .trim()
 | 
			
		||||
                                            .takeIf { it.isNotBlank() }
 | 
			
		||||
                            )
 | 
			
		||||
                        } else {
 | 
			
		||||
                            ParsedCommand.ErrorSyntax(Command.PART)
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        ParsedCommand.ErrorSyntax(Command.PART)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Command.INVITE.command                 -> {
 | 
			
		||||
                    if (messageParts.size == 2) {
 | 
			
		||||
                    if (messageParts.size >= 2) {
 | 
			
		||||
                        val userId = messageParts[1]
 | 
			
		||||
 | 
			
		||||
                        if (MatrixPatterns.isUserId(userId)) {
 | 
			
		||||
                            ParsedCommand.Invite(userId)
 | 
			
		||||
                            ParsedCommand.Invite(
 | 
			
		||||
                                    userId,
 | 
			
		||||
                                    textMessage.substring(Command.INVITE.length + userId.length)
 | 
			
		||||
                                            .trim()
 | 
			
		||||
                                            .takeIf { it.isNotBlank() }
 | 
			
		||||
                            )
 | 
			
		||||
                        } else {
 | 
			
		||||
                            ParsedCommand.ErrorSyntax(Command.INVITE)
 | 
			
		||||
                        }
 | 
			
		||||
@ -114,12 +137,14 @@ object CommandParser {
 | 
			
		||||
                Command.KICK_USER.command              -> {
 | 
			
		||||
                    if (messageParts.size >= 2) {
 | 
			
		||||
                        val userId = messageParts[1]
 | 
			
		||||
                        if (MatrixPatterns.isUserId(userId)) {
 | 
			
		||||
                            val reason = textMessage.substring(Command.KICK_USER.command.length
 | 
			
		||||
                                    + 1
 | 
			
		||||
                                    + userId.length).trim()
 | 
			
		||||
 | 
			
		||||
                            ParsedCommand.KickUser(userId, reason)
 | 
			
		||||
                        if (MatrixPatterns.isUserId(userId)) {
 | 
			
		||||
                            ParsedCommand.KickUser(
 | 
			
		||||
                                    userId,
 | 
			
		||||
                                    textMessage.substring(Command.KICK_USER.length + userId.length)
 | 
			
		||||
                                            .trim()
 | 
			
		||||
                                            .takeIf { it.isNotBlank() }
 | 
			
		||||
                            )
 | 
			
		||||
                        } else {
 | 
			
		||||
                            ParsedCommand.ErrorSyntax(Command.KICK_USER)
 | 
			
		||||
                        }
 | 
			
		||||
@ -130,12 +155,14 @@ object CommandParser {
 | 
			
		||||
                Command.BAN_USER.command               -> {
 | 
			
		||||
                    if (messageParts.size >= 2) {
 | 
			
		||||
                        val userId = messageParts[1]
 | 
			
		||||
                        if (MatrixPatterns.isUserId(userId)) {
 | 
			
		||||
                            val reason = textMessage.substring(Command.BAN_USER.command.length
 | 
			
		||||
                                    + 1
 | 
			
		||||
                                    + userId.length).trim()
 | 
			
		||||
 | 
			
		||||
                            ParsedCommand.BanUser(userId, reason)
 | 
			
		||||
                        if (MatrixPatterns.isUserId(userId)) {
 | 
			
		||||
                            ParsedCommand.BanUser(
 | 
			
		||||
                                    userId,
 | 
			
		||||
                                    textMessage.substring(Command.BAN_USER.length + userId.length)
 | 
			
		||||
                                            .trim()
 | 
			
		||||
                                            .takeIf { it.isNotBlank() }
 | 
			
		||||
                            )
 | 
			
		||||
                        } else {
 | 
			
		||||
                            ParsedCommand.ErrorSyntax(Command.BAN_USER)
 | 
			
		||||
                        }
 | 
			
		||||
@ -144,11 +171,16 @@ object CommandParser {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Command.UNBAN_USER.command             -> {
 | 
			
		||||
                    if (messageParts.size == 2) {
 | 
			
		||||
                    if (messageParts.size >= 2) {
 | 
			
		||||
                        val userId = messageParts[1]
 | 
			
		||||
 | 
			
		||||
                        if (MatrixPatterns.isUserId(userId)) {
 | 
			
		||||
                            ParsedCommand.UnbanUser(userId)
 | 
			
		||||
                            ParsedCommand.UnbanUser(
 | 
			
		||||
                                    userId,
 | 
			
		||||
                                    textMessage.substring(Command.UNBAN_USER.length + userId.length)
 | 
			
		||||
                                            .trim()
 | 
			
		||||
                                            .takeIf { it.isNotBlank() }
 | 
			
		||||
                            )
 | 
			
		||||
                        } else {
 | 
			
		||||
                            ParsedCommand.ErrorSyntax(Command.UNBAN_USER)
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
@ -34,14 +34,14 @@ sealed class ParsedCommand {
 | 
			
		||||
    // Valid commands:
 | 
			
		||||
 | 
			
		||||
    class SendEmote(val message: CharSequence) : ParsedCommand()
 | 
			
		||||
    class BanUser(val userId: String, val reason: String) : ParsedCommand()
 | 
			
		||||
    class UnbanUser(val userId: String) : ParsedCommand()
 | 
			
		||||
    class BanUser(val userId: String, val reason: String?) : ParsedCommand()
 | 
			
		||||
    class UnbanUser(val userId: String, val reason: String?) : ParsedCommand()
 | 
			
		||||
    class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand()
 | 
			
		||||
    class Invite(val userId: String) : ParsedCommand()
 | 
			
		||||
    class JoinRoom(val roomAlias: String) : ParsedCommand()
 | 
			
		||||
    class PartRoom(val roomAlias: String) : ParsedCommand()
 | 
			
		||||
    class Invite(val userId: String, val reason: String?) : ParsedCommand()
 | 
			
		||||
    class JoinRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
 | 
			
		||||
    class PartRoom(val roomAlias: String, val reason: String?) : ParsedCommand()
 | 
			
		||||
    class ChangeTopic(val topic: String) : ParsedCommand()
 | 
			
		||||
    class KickUser(val userId: String, val reason: String) : ParsedCommand()
 | 
			
		||||
    class KickUser(val userId: String, val reason: String?) : ParsedCommand()
 | 
			
		||||
    class ChangeDisplayName(val displayName: String) : ParsedCommand()
 | 
			
		||||
    class SetMarkdown(val enable: Boolean) : ParsedCommand()
 | 
			
		||||
    object ClearScalarToken : ParsedCommand()
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,8 @@ import androidx.appcompat.app.AlertDialog
 | 
			
		||||
import com.airbnb.mvrx.activityViewModel
 | 
			
		||||
import com.airbnb.mvrx.withState
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.extensions.cleanup
 | 
			
		||||
import im.vector.riotx.core.extensions.configureWith
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseFragment
 | 
			
		||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
 | 
			
		||||
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
 | 
			
		||||
@ -37,12 +39,16 @@ class KeysBackupSettingsFragment @Inject constructor(private val keysBackupSetti
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        keysBackupSettingsRecyclerView.setController(keysBackupSettingsRecyclerViewController)
 | 
			
		||||
 | 
			
		||||
        keysBackupSettingsRecyclerView.configureWith(keysBackupSettingsRecyclerViewController)
 | 
			
		||||
        keysBackupSettingsRecyclerViewController.listener = this
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        keysBackupSettingsRecyclerViewController.listener = null
 | 
			
		||||
        keysBackupSettingsRecyclerView.cleanup()
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun invalidate() = withState(viewModel) { state ->
 | 
			
		||||
        keysBackupSettingsRecyclerViewController.setData(state)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,6 @@ import com.airbnb.mvrx.fragmentViewModel
 | 
			
		||||
import com.airbnb.mvrx.withState
 | 
			
		||||
import com.google.android.material.bottomnavigation.BottomNavigationItemView
 | 
			
		||||
import com.google.android.material.bottomnavigation.BottomNavigationMenuView
 | 
			
		||||
import im.vector.matrix.android.api.session.Session
 | 
			
		||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
 | 
			
		||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
@ -46,7 +45,6 @@ private const val INDEX_PEOPLE = 1
 | 
			
		||||
private const val INDEX_ROOMS = 2
 | 
			
		||||
 | 
			
		||||
class HomeDetailFragment @Inject constructor(
 | 
			
		||||
        private val session: Session,
 | 
			
		||||
        val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
 | 
			
		||||
        private val avatarRenderer: AvatarRenderer
 | 
			
		||||
) : VectorBaseFragment(), KeysBackupBanner.Delegate {
 | 
			
		||||
@ -56,9 +54,7 @@ class HomeDetailFragment @Inject constructor(
 | 
			
		||||
    private val viewModel: HomeDetailViewModel by fragmentViewModel()
 | 
			
		||||
    private lateinit var sharedActionViewModel: HomeSharedActionViewModel
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutResId(): Int {
 | 
			
		||||
        return R.layout.fragment_home_detail
 | 
			
		||||
    }
 | 
			
		||||
    override fun getLayoutResId() = R.layout.fragment_home_detail
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
 | 
			
		||||
@ -23,9 +23,7 @@ import com.airbnb.mvrx.withState
 | 
			
		||||
import com.jakewharton.rxbinding3.widget.textChanges
 | 
			
		||||
import im.vector.matrix.android.api.session.user.model.User
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.riotx.core.extensions.setupAsSearch
 | 
			
		||||
import im.vector.riotx.core.extensions.showKeyboard
 | 
			
		||||
import im.vector.riotx.core.extensions.*
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseFragment
 | 
			
		||||
import kotlinx.android.synthetic.main.fragment_create_direct_room_directory_users.*
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
@ -48,10 +46,15 @@ class CreateDirectRoomDirectoryUsersFragment @Inject constructor(
 | 
			
		||||
        setupCloseView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        recyclerView.cleanup()
 | 
			
		||||
        directRoomController.callback = null
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupRecyclerView() {
 | 
			
		||||
        recyclerView.setHasFixedSize(true)
 | 
			
		||||
        directRoomController.callback = this
 | 
			
		||||
        recyclerView.setController(directRoomController)
 | 
			
		||||
        recyclerView.configureWith(directRoomController)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupSearchByMatrixIdView() {
 | 
			
		||||
 | 
			
		||||
@ -31,9 +31,7 @@ import com.google.android.material.chip.ChipGroup
 | 
			
		||||
import com.jakewharton.rxbinding3.widget.textChanges
 | 
			
		||||
import im.vector.matrix.android.api.session.user.model.User
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.riotx.core.extensions.observeEvent
 | 
			
		||||
import im.vector.riotx.core.extensions.setupAsSearch
 | 
			
		||||
import im.vector.riotx.core.extensions.*
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseFragment
 | 
			
		||||
import im.vector.riotx.core.utils.DimensionConverter
 | 
			
		||||
import kotlinx.android.synthetic.main.fragment_create_direct_room.*
 | 
			
		||||
@ -67,6 +65,12 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        knownUsersController.callback = null
 | 
			
		||||
        recyclerView.cleanup()
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareOptionsMenu(menu: Menu) {
 | 
			
		||||
        withState(viewModel) {
 | 
			
		||||
            val createMenuItem = menu.findItem(R.id.action_create_direct_room)
 | 
			
		||||
@ -94,11 +98,10 @@ class CreateDirectRoomKnownUsersFragment @Inject constructor(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupRecyclerView() {
 | 
			
		||||
        recyclerView.setHasFixedSize(true)
 | 
			
		||||
        // Don't activate animation as we might have way to much item animation when filtering
 | 
			
		||||
        recyclerView.itemAnimator = null
 | 
			
		||||
        knownUsersController.callback = this
 | 
			
		||||
        recyclerView.setController(knownUsersController)
 | 
			
		||||
        recyclerView.configureWith(knownUsersController)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupFilterView() {
 | 
			
		||||
 | 
			
		||||
@ -23,11 +23,13 @@ import com.airbnb.mvrx.Success
 | 
			
		||||
import com.airbnb.mvrx.fragmentViewModel
 | 
			
		||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.extensions.cleanup
 | 
			
		||||
import im.vector.riotx.core.extensions.configureWith
 | 
			
		||||
import im.vector.riotx.core.extensions.observeEvent
 | 
			
		||||
import im.vector.riotx.core.platform.StateView
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseFragment
 | 
			
		||||
import im.vector.riotx.features.home.HomeSharedActionViewModel
 | 
			
		||||
import im.vector.riotx.features.home.HomeActivitySharedAction
 | 
			
		||||
import im.vector.riotx.features.home.HomeSharedActionViewModel
 | 
			
		||||
import kotlinx.android.synthetic.main.fragment_group_list.*
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
@ -45,14 +47,20 @@ class GroupListFragment @Inject constructor(
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
        sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
 | 
			
		||||
        groupController.callback = this
 | 
			
		||||
        stateView.contentView = groupListEpoxyRecyclerView
 | 
			
		||||
        groupListEpoxyRecyclerView.setController(groupController)
 | 
			
		||||
        stateView.contentView = groupListView
 | 
			
		||||
        groupListView.configureWith(groupController)
 | 
			
		||||
        viewModel.subscribe { renderState(it) }
 | 
			
		||||
        viewModel.openGroupLiveData.observeEvent(this) {
 | 
			
		||||
            sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        groupController.callback = null
 | 
			
		||||
        groupListView.cleanup()
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun renderState(state: GroupListViewState) {
 | 
			
		||||
        when (state.asyncGroups) {
 | 
			
		||||
            is Incomplete -> stateView.state = StateView.State.Loading
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
                }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -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
 | 
			
		||||
@ -20,17 +20,25 @@ import android.content.Context
 | 
			
		||||
import android.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import androidx.appcompat.widget.Toolbar
 | 
			
		||||
import androidx.core.view.GravityCompat
 | 
			
		||||
import androidx.drawerlayout.widget.DrawerLayout
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.riotx.core.extensions.replaceFragment
 | 
			
		||||
import im.vector.riotx.core.platform.ToolbarConfigurable
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseActivity
 | 
			
		||||
import im.vector.riotx.features.home.room.breadcrumbs.BreadcrumbsFragment
 | 
			
		||||
import kotlinx.android.synthetic.main.activity_room_detail.*
 | 
			
		||||
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
 | 
			
		||||
 | 
			
		||||
class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.activity_room_detail
 | 
			
		||||
    }
 | 
			
		||||
    override fun getLayoutRes() = R.layout.activity_room_detail
 | 
			
		||||
 | 
			
		||||
    private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel
 | 
			
		||||
 | 
			
		||||
    // Simple filter
 | 
			
		||||
    private var currentRoomId: String? = null
 | 
			
		||||
 | 
			
		||||
    override fun onCreate(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onCreate(savedInstanceState)
 | 
			
		||||
@ -38,14 +46,57 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
 | 
			
		||||
        if (isFirstCreation()) {
 | 
			
		||||
            val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS)
 | 
			
		||||
                    ?: return
 | 
			
		||||
            currentRoomId = roomDetailArgs.roomId
 | 
			
		||||
            replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, roomDetailArgs)
 | 
			
		||||
            replaceFragment(R.id.roomDetailDrawerContainer, BreadcrumbsFragment::class.java)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        sharedActionViewModel = viewModelProvider.get(RoomDetailSharedActionViewModel::class.java)
 | 
			
		||||
 | 
			
		||||
        sharedActionViewModel
 | 
			
		||||
                .observe()
 | 
			
		||||
                .subscribe { sharedAction ->
 | 
			
		||||
                    when (sharedAction) {
 | 
			
		||||
                        is RoomDetailSharedAction.SwitchToRoom -> switchToRoom(sharedAction)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                .disposeOnDestroy()
 | 
			
		||||
 | 
			
		||||
        drawerLayout.addDrawerListener(drawerListener)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun switchToRoom(switchToRoom: RoomDetailSharedAction.SwitchToRoom) {
 | 
			
		||||
        drawerLayout.closeDrawer(GravityCompat.START)
 | 
			
		||||
        // Do not replace the Fragment if it's the same roomId
 | 
			
		||||
        if (currentRoomId != switchToRoom.roomId) {
 | 
			
		||||
            currentRoomId = switchToRoom.roomId
 | 
			
		||||
            replaceFragment(R.id.roomDetailContainer, RoomDetailFragment::class.java, RoomDetailArgs(switchToRoom.roomId))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroy() {
 | 
			
		||||
        drawerLayout.removeDrawerListener(drawerListener)
 | 
			
		||||
        super.onDestroy()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun configure(toolbar: Toolbar) {
 | 
			
		||||
        configureToolbar(toolbar)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
 | 
			
		||||
        override fun onDrawerStateChanged(newState: Int) {
 | 
			
		||||
            hideKeyboard()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBackPressed() {
 | 
			
		||||
        if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
 | 
			
		||||
            drawerLayout.closeDrawer(GravityCompat.START)
 | 
			
		||||
        } else {
 | 
			
		||||
            super.onBackPressed()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
 | 
			
		||||
        private const val EXTRA_ROOM_DETAIL_ARGS = "EXTRA_ROOM_DETAIL_ARGS"
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,7 @@ import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import butterknife.BindView
 | 
			
		||||
import com.airbnb.epoxy.EpoxyModel
 | 
			
		||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
 | 
			
		||||
import com.airbnb.epoxy.OnModelBuildFinishedListener
 | 
			
		||||
import com.airbnb.mvrx.*
 | 
			
		||||
import com.github.piasy.biv.BigImageViewer
 | 
			
		||||
import com.github.piasy.biv.loader.ImageLoader
 | 
			
		||||
@ -69,10 +70,7 @@ import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.dialogs.withColoredButton
 | 
			
		||||
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
 | 
			
		||||
import im.vector.riotx.core.error.ErrorFormatter
 | 
			
		||||
import im.vector.riotx.core.extensions.hideKeyboard
 | 
			
		||||
import im.vector.riotx.core.extensions.observeEvent
 | 
			
		||||
import im.vector.riotx.core.extensions.setTextOrHide
 | 
			
		||||
import im.vector.riotx.core.extensions.showKeyboard
 | 
			
		||||
import im.vector.riotx.core.extensions.*
 | 
			
		||||
import im.vector.riotx.core.files.addEntryToDownloadManager
 | 
			
		||||
import im.vector.riotx.core.glide.GlideApp
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseFragment
 | 
			
		||||
@ -194,6 +192,8 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
 | 
			
		||||
    private lateinit var sharedActionViewModel: MessageSharedActionViewModel
 | 
			
		||||
    private lateinit var layoutManager: LinearLayoutManager
 | 
			
		||||
    private var modelBuildListener: OnModelBuildFinishedListener? = null
 | 
			
		||||
 | 
			
		||||
    private lateinit var attachmentsHelper: AttachmentsHelper
 | 
			
		||||
    private lateinit var keyboardStateUtils: KeyboardStateUtils
 | 
			
		||||
 | 
			
		||||
@ -297,13 +297,16 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        timelineEventController.callback = null
 | 
			
		||||
        timelineEventController.removeModelBuildListener(modelBuildListener)
 | 
			
		||||
        modelBuildListener = null
 | 
			
		||||
        debouncer.cancelAll()
 | 
			
		||||
        recyclerView.cleanup()
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
        recyclerView.adapter = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroy() {
 | 
			
		||||
        roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
 | 
			
		||||
        debouncer.cancelAll()
 | 
			
		||||
        super.onDestroy()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -458,11 +461,7 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
        if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
 | 
			
		||||
            when (requestCode) {
 | 
			
		||||
                REACTION_SELECT_REQUEST_CODE -> {
 | 
			
		||||
                    val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
 | 
			
		||||
                            ?: return
 | 
			
		||||
                    val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
 | 
			
		||||
                            ?: return
 | 
			
		||||
                    // TODO check if already reacted with that?
 | 
			
		||||
                    val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return
 | 
			
		||||
                    roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@ -481,13 +480,14 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
        recyclerView.layoutManager = layoutManager
 | 
			
		||||
        recyclerView.itemAnimator = null
 | 
			
		||||
        recyclerView.setHasFixedSize(true)
 | 
			
		||||
        timelineEventController.addModelBuildListener {
 | 
			
		||||
        modelBuildListener = OnModelBuildFinishedListener {
 | 
			
		||||
            it.dispatchTo(stateRestorer)
 | 
			
		||||
            it.dispatchTo(scrollOnNewMessageCallback)
 | 
			
		||||
            it.dispatchTo(scrollOnHighlightedEventCallback)
 | 
			
		||||
            updateJumpToReadMarkerViewVisibility()
 | 
			
		||||
            updateJumpToBottomViewVisibility()
 | 
			
		||||
        }
 | 
			
		||||
        timelineEventController.addModelBuildListener(modelBuildListener)
 | 
			
		||||
        recyclerView.adapter = timelineEventController.adapter
 | 
			
		||||
 | 
			
		||||
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
 | 
			
		||||
@ -532,27 +532,29 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun updateJumpToReadMarkerViewVisibility() = jumpToReadMarkerView.post {
 | 
			
		||||
        withState(roomDetailViewModel) {
 | 
			
		||||
            val showJumpToUnreadBanner = when (it.unreadState) {
 | 
			
		||||
                UnreadState.Unknown,
 | 
			
		||||
                UnreadState.HasNoUnread            -> false
 | 
			
		||||
                is UnreadState.ReadMarkerNotLoaded -> true
 | 
			
		||||
                is UnreadState.HasUnread           -> {
 | 
			
		||||
                    if (it.canShowJumpToReadMarker) {
 | 
			
		||||
                        val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
 | 
			
		||||
                        val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
 | 
			
		||||
                        if (positionOfReadMarker == null) {
 | 
			
		||||
                            false
 | 
			
		||||
    private fun updateJumpToReadMarkerViewVisibility() {
 | 
			
		||||
        jumpToReadMarkerView?.post {
 | 
			
		||||
            withState(roomDetailViewModel) {
 | 
			
		||||
                val showJumpToUnreadBanner = when (it.unreadState) {
 | 
			
		||||
                    UnreadState.Unknown,
 | 
			
		||||
                    UnreadState.HasNoUnread            -> false
 | 
			
		||||
                    is UnreadState.ReadMarkerNotLoaded -> true
 | 
			
		||||
                    is UnreadState.HasUnread           -> {
 | 
			
		||||
                        if (it.canShowJumpToReadMarker) {
 | 
			
		||||
                            val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
 | 
			
		||||
                            val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
 | 
			
		||||
                            if (positionOfReadMarker == null) {
 | 
			
		||||
                                false
 | 
			
		||||
                            } else {
 | 
			
		||||
                                positionOfReadMarker > lastVisibleItem
 | 
			
		||||
                            }
 | 
			
		||||
                        } else {
 | 
			
		||||
                            positionOfReadMarker > lastVisibleItem
 | 
			
		||||
                            false
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        false
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
 | 
			
		||||
            }
 | 
			
		||||
            jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1192,7 +1194,7 @@ class RoomDetailFragment @Inject constructor(
 | 
			
		||||
                && userId == session.myUserId) {
 | 
			
		||||
            // Empty composer, current user: start an emote
 | 
			
		||||
            composerLayout.composerEditText.setText(Command.EMOTE.command + " ")
 | 
			
		||||
            composerLayout.composerEditText.setSelection(Command.EMOTE.command.length + 1)
 | 
			
		||||
            composerLayout.composerEditText.setSelection(Command.EMOTE.length)
 | 
			
		||||
        } else {
 | 
			
		||||
            val roomMember = roomDetailViewModel.getMember(userId)
 | 
			
		||||
            // TODO move logic outside of fragment
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
}
 | 
			
		||||
@ -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>()
 | 
			
		||||
@ -149,6 +149,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
 | 
			
		||||
        timeline.addListener(this)
 | 
			
		||||
        timeline.start()
 | 
			
		||||
        setState { copy(timeline = this@RoomDetailViewModel.timeline) }
 | 
			
		||||
 | 
			
		||||
        // Inform the SDK that the room is displayed
 | 
			
		||||
        session.onRoomDisplayed(initialState.roomId)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun handle(action: RoomDetailAction) {
 | 
			
		||||
@ -270,7 +273,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            session.rx()
 | 
			
		||||
                    .joinRoom(roomId, viaServer)
 | 
			
		||||
                    .joinRoom(roomId, viaServers = viaServer)
 | 
			
		||||
                    .map { roomId }
 | 
			
		||||
                    .execute {
 | 
			
		||||
                        copy(tombstoneEventHandling = it)
 | 
			
		||||
@ -491,7 +494,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
 | 
			
		||||
    private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
 | 
			
		||||
        _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) {
 | 
			
		||||
                _sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk)
 | 
			
		||||
            }
 | 
			
		||||
@ -557,7 +560,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleRejectInvite() {
 | 
			
		||||
        room.leave(object : MatrixCallback<Unit> {})
 | 
			
		||||
        room.leave(null, object : MatrixCallback<Unit> {})
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleAcceptInvite() {
 | 
			
		||||
@ -877,7 +880,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
 | 
			
		||||
 | 
			
		||||
    override fun onCleared() {
 | 
			
		||||
        timeline.dispose()
 | 
			
		||||
        timeline.removeAllListeners()
 | 
			
		||||
        timeline.removeListener(this)
 | 
			
		||||
        super.onCleared()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,6 @@ import android.os.Parcelable
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import butterknife.BindView
 | 
			
		||||
import butterknife.ButterKnife
 | 
			
		||||
@ -29,6 +28,8 @@ import com.airbnb.mvrx.MvRx
 | 
			
		||||
import com.airbnb.mvrx.args
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.di.ScreenComponent
 | 
			
		||||
import im.vector.riotx.core.extensions.cleanup
 | 
			
		||||
import im.vector.riotx.core.extensions.configureWith
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
 | 
			
		||||
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
 | 
			
		||||
import kotlinx.android.parcel.Parcelize
 | 
			
		||||
@ -52,8 +53,8 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
 | 
			
		||||
 | 
			
		||||
    private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
 | 
			
		||||
 | 
			
		||||
    override fun injectWith(screenComponent: ScreenComponent) {
 | 
			
		||||
        screenComponent.inject(this)
 | 
			
		||||
    override fun injectWith(injector: ScreenComponent) {
 | 
			
		||||
        injector.inject(this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
 | 
			
		||||
@ -64,12 +65,16 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
 | 
			
		||||
 | 
			
		||||
    override fun onActivityCreated(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onActivityCreated(savedInstanceState)
 | 
			
		||||
        recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
 | 
			
		||||
        recyclerView.adapter = epoxyController.adapter
 | 
			
		||||
        recyclerView.configureWith(epoxyController, hasFixedSize = false)
 | 
			
		||||
        bottomSheetTitle.text = getString(R.string.seen_by)
 | 
			
		||||
        epoxyController.setData(displayReadReceiptArgs.readReceipts)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        recyclerView.cleanup()
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // we are not using state for this one as it's static, so no need to override invalidate()
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,6 @@ import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import com.airbnb.epoxy.EpoxyController
 | 
			
		||||
import com.airbnb.epoxy.EpoxyModel
 | 
			
		||||
import com.airbnb.epoxy.VisibilityState
 | 
			
		||||
import im.vector.matrix.android.api.session.Session
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.message.*
 | 
			
		||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
 | 
			
		||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 | 
			
		||||
@ -44,7 +43,7 @@ import org.threeten.bp.LocalDateTime
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
 | 
			
		||||
                                                  private val session: Session,
 | 
			
		||||
                                                  private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
 | 
			
		||||
                                                  private val timelineItemFactory: TimelineItemFactory,
 | 
			
		||||
                                                  private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
 | 
			
		||||
                                                  private val mergedHeaderItemFactory: MergedHeaderItemFactory,
 | 
			
		||||
@ -209,6 +208,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
 | 
			
		||||
        timelineMediaSizeProvider.recyclerView = recyclerView
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
 | 
			
		||||
        timelineMediaSizeProvider.recyclerView = null
 | 
			
		||||
        contentUploadStateTrackerBinder.clear()
 | 
			
		||||
        timeline?.removeListener(this)
 | 
			
		||||
        super.onDetachedFromRecyclerView(recyclerView)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun buildModels() {
 | 
			
		||||
        val timestamp = System.currentTimeMillis()
 | 
			
		||||
        showingForwardLoader = LoadingItem_()
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -19,7 +19,6 @@ import android.os.Bundle
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import butterknife.BindView
 | 
			
		||||
import butterknife.ButterKnife
 | 
			
		||||
@ -27,6 +26,8 @@ import com.airbnb.mvrx.fragmentViewModel
 | 
			
		||||
import com.airbnb.mvrx.withState
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.di.ScreenComponent
 | 
			
		||||
import im.vector.riotx.core.extensions.cleanup
 | 
			
		||||
import im.vector.riotx.core.extensions.configureWith
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
 | 
			
		||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
@ -48,8 +49,8 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
 | 
			
		||||
 | 
			
		||||
    private lateinit var sharedActionViewModel: MessageSharedActionViewModel
 | 
			
		||||
 | 
			
		||||
    override fun injectWith(screenComponent: ScreenComponent) {
 | 
			
		||||
        screenComponent.inject(this)
 | 
			
		||||
    override fun injectWith(injector: ScreenComponent) {
 | 
			
		||||
        injector.inject(this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
 | 
			
		||||
@ -61,13 +62,17 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
 | 
			
		||||
    override fun onActivityCreated(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onActivityCreated(savedInstanceState)
 | 
			
		||||
        sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
 | 
			
		||||
        recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
 | 
			
		||||
        recyclerView.adapter = messageActionsEpoxyController.adapter
 | 
			
		||||
        recyclerView.configureWith(messageActionsEpoxyController, hasFixedSize = false)
 | 
			
		||||
        // Disable item animation
 | 
			
		||||
        recyclerView.itemAnimator = null
 | 
			
		||||
        messageActionsEpoxyController.listener = this
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        recyclerView.cleanup()
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onUrlClicked(url: String): Boolean {
 | 
			
		||||
        sharedActionViewModel.post(EventSharedAction.OnUrlClicked(url))
 | 
			
		||||
        // Always consume
 | 
			
		||||
@ -83,6 +88,10 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), Message
 | 
			
		||||
    override fun didSelectMenuAction(eventAction: EventSharedAction) {
 | 
			
		||||
        if (eventAction is EventSharedAction.ReportContent) {
 | 
			
		||||
            // Toggle report menu
 | 
			
		||||
            // Enable item animation
 | 
			
		||||
            if (recyclerView.itemAnimator == null) {
 | 
			
		||||
                recyclerView.itemAnimator = MessageActionsAnimator()
 | 
			
		||||
            }
 | 
			
		||||
            viewModel.handle(MessageActionsAction.ToggleReportMenu)
 | 
			
		||||
        } else {
 | 
			
		||||
            sharedActionViewModel.post(eventAction)
 | 
			
		||||
 | 
			
		||||
@ -19,9 +19,6 @@ import android.os.Bundle
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import android.widget.LinearLayout
 | 
			
		||||
import androidx.recyclerview.widget.DividerItemDecoration
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import butterknife.BindView
 | 
			
		||||
import butterknife.ButterKnife
 | 
			
		||||
@ -30,6 +27,8 @@ import com.airbnb.mvrx.fragmentViewModel
 | 
			
		||||
import com.airbnb.mvrx.withState
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.di.ScreenComponent
 | 
			
		||||
import im.vector.riotx.core.extensions.cleanup
 | 
			
		||||
import im.vector.riotx.core.extensions.configureWith
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
 | 
			
		||||
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
 | 
			
		||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
 | 
			
		||||
@ -54,8 +53,8 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
 | 
			
		||||
        ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun injectWith(screenComponent: ScreenComponent) {
 | 
			
		||||
        screenComponent.inject(this)
 | 
			
		||||
    override fun injectWith(injector: ScreenComponent) {
 | 
			
		||||
        injector.inject(this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
 | 
			
		||||
@ -66,13 +65,18 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
 | 
			
		||||
 | 
			
		||||
    override fun onActivityCreated(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onActivityCreated(savedInstanceState)
 | 
			
		||||
        recyclerView.adapter = epoxyController.adapter
 | 
			
		||||
        recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
 | 
			
		||||
        val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayout.VERTICAL)
 | 
			
		||||
        recyclerView.addItemDecoration(dividerItemDecoration)
 | 
			
		||||
        recyclerView.configureWith(
 | 
			
		||||
                epoxyController,
 | 
			
		||||
                showDivider = true,
 | 
			
		||||
                hasFixedSize = false)
 | 
			
		||||
        bottomSheetTitle.text = context?.getString(R.string.message_edits)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        recyclerView.cleanup()
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun invalidate() = withState(viewModel) {
 | 
			
		||||
        epoxyController.setData(it)
 | 
			
		||||
        super.invalidate()
 | 
			
		||||
 | 
			
		||||
@ -28,17 +28,16 @@ import im.vector.matrix.android.api.session.events.model.toModel
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
 | 
			
		||||
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.date.VectorDateFormatter
 | 
			
		||||
import im.vector.riotx.core.extensions.localDateTime
 | 
			
		||||
import im.vector.riotx.core.ui.list.genericFooterItem
 | 
			
		||||
import im.vector.riotx.core.ui.list.genericItem
 | 
			
		||||
import im.vector.riotx.core.ui.list.genericItemHeader
 | 
			
		||||
import im.vector.riotx.core.ui.list.genericLoaderItem
 | 
			
		||||
import im.vector.riotx.core.date.VectorDateFormatter
 | 
			
		||||
import im.vector.riotx.features.html.EventHtmlRenderer
 | 
			
		||||
import me.gujun.android.span.span
 | 
			
		||||
import name.fraser.neil.plaintext.diff_match_patch
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
import java.util.Calendar
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Epoxy controller for edit history list
 | 
			
		||||
@ -104,9 +103,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
 | 
			
		||||
                            ?: nContent.first
 | 
			
		||||
                    val dmp = diff_match_patch()
 | 
			
		||||
                    val diff = dmp.diff_main(nextBody.toString(), body.toString())
 | 
			
		||||
                    Timber.e("#### Diff: $diff")
 | 
			
		||||
                    dmp.diff_cleanupSemantic(diff)
 | 
			
		||||
                    Timber.e("#### Diff: $diff")
 | 
			
		||||
                    spannedDiff = span {
 | 
			
		||||
                        diff.map {
 | 
			
		||||
                            when (it.operation) {
 | 
			
		||||
 | 
			
		||||
@ -19,14 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.format
 | 
			
		||||
import im.vector.matrix.android.api.session.events.model.Event
 | 
			
		||||
import im.vector.matrix.android.api.session.events.model.EventType
 | 
			
		||||
import im.vector.matrix.android.api.session.events.model.toModel
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.Membership
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.RoomJoinRules
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.RoomJoinRulesContent
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.RoomMember
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.RoomNameContent
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.*
 | 
			
		||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
 | 
			
		||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
@ -36,7 +29,7 @@ import timber.log.Timber
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
class NoticeEventFormatter @Inject constructor(private val sessionHolder: ActiveSessionHolder,
 | 
			
		||||
                                               private val stringProvider: StringProvider) {
 | 
			
		||||
                                               private val sp: StringProvider) {
 | 
			
		||||
 | 
			
		||||
    fun format(timelineEvent: TimelineEvent): CharSequence? {
 | 
			
		||||
        return when (val type = timelineEvent.root.getClearType()) {
 | 
			
		||||
@ -84,36 +77,35 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
 | 
			
		||||
    private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
 | 
			
		||||
        val content = event.getClearContent().toModel<RoomNameContent>() ?: return null
 | 
			
		||||
        return if (content.name.isNullOrBlank()) {
 | 
			
		||||
            stringProvider.getString(R.string.notice_room_name_removed, senderName)
 | 
			
		||||
            sp.getString(R.string.notice_room_name_removed, senderName)
 | 
			
		||||
        } else {
 | 
			
		||||
            stringProvider.getString(R.string.notice_room_name_changed, senderName, content.name)
 | 
			
		||||
            sp.getString(R.string.notice_room_name_changed, senderName, content.name)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun formatRoomTombstoneEvent(senderName: String?): CharSequence? {
 | 
			
		||||
        return stringProvider.getString(R.string.notice_room_update, senderName)
 | 
			
		||||
        return sp.getString(R.string.notice_room_update, senderName)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun formatRoomTopicEvent(event: Event, senderName: String?): CharSequence? {
 | 
			
		||||
        val content = event.getClearContent().toModel<RoomTopicContent>() ?: return null
 | 
			
		||||
        return if (content.topic.isNullOrEmpty()) {
 | 
			
		||||
            stringProvider.getString(R.string.notice_room_topic_removed, senderName)
 | 
			
		||||
            sp.getString(R.string.notice_room_topic_removed, senderName)
 | 
			
		||||
        } else {
 | 
			
		||||
            stringProvider.getString(R.string.notice_room_topic_changed, senderName, content.topic)
 | 
			
		||||
            sp.getString(R.string.notice_room_topic_changed, senderName, content.topic)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
 | 
			
		||||
        val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility
 | 
			
		||||
                                ?: return null
 | 
			
		||||
        val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
 | 
			
		||||
 | 
			
		||||
        val formattedVisibility = when (historyVisibility) {
 | 
			
		||||
            RoomHistoryVisibility.SHARED         -> stringProvider.getString(R.string.notice_room_visibility_shared)
 | 
			
		||||
            RoomHistoryVisibility.INVITED        -> stringProvider.getString(R.string.notice_room_visibility_invited)
 | 
			
		||||
            RoomHistoryVisibility.JOINED         -> stringProvider.getString(R.string.notice_room_visibility_joined)
 | 
			
		||||
            RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable)
 | 
			
		||||
            RoomHistoryVisibility.SHARED         -> sp.getString(R.string.notice_room_visibility_shared)
 | 
			
		||||
            RoomHistoryVisibility.INVITED        -> sp.getString(R.string.notice_room_visibility_invited)
 | 
			
		||||
            RoomHistoryVisibility.JOINED         -> sp.getString(R.string.notice_room_visibility_joined)
 | 
			
		||||
            RoomHistoryVisibility.WORLD_READABLE -> sp.getString(R.string.notice_room_visibility_world_readable)
 | 
			
		||||
        }
 | 
			
		||||
        return stringProvider.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility)
 | 
			
		||||
        return sp.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun formatCallEvent(event: Event, senderName: String?): CharSequence? {
 | 
			
		||||
@ -122,13 +114,13 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
 | 
			
		||||
                val content = event.getClearContent().toModel<CallInviteContent>() ?: return null
 | 
			
		||||
                val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
 | 
			
		||||
                return if (isVideoCall) {
 | 
			
		||||
                    stringProvider.getString(R.string.notice_placed_video_call, senderName)
 | 
			
		||||
                    sp.getString(R.string.notice_placed_video_call, senderName)
 | 
			
		||||
                } else {
 | 
			
		||||
                    stringProvider.getString(R.string.notice_placed_voice_call, senderName)
 | 
			
		||||
                    sp.getString(R.string.notice_placed_voice_call, senderName)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, senderName)
 | 
			
		||||
            EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, senderName)
 | 
			
		||||
            EventType.CALL_ANSWER == event.type -> sp.getString(R.string.notice_answered_call, senderName)
 | 
			
		||||
            EventType.CALL_HANGUP == event.type -> sp.getString(R.string.notice_ended_call, senderName)
 | 
			
		||||
            else                                -> null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -150,12 +142,11 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
 | 
			
		||||
        if (eventContent?.displayName != prevEventContent?.displayName) {
 | 
			
		||||
            val displayNameText = when {
 | 
			
		||||
                prevEventContent?.displayName.isNullOrEmpty() ->
 | 
			
		||||
                    stringProvider.getString(R.string.notice_display_name_set, event.senderId, eventContent?.displayName)
 | 
			
		||||
                    sp.getString(R.string.notice_display_name_set, event.senderId, eventContent?.displayName)
 | 
			
		||||
                eventContent?.displayName.isNullOrEmpty()     ->
 | 
			
		||||
                    stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName)
 | 
			
		||||
                    sp.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName)
 | 
			
		||||
                else                                          ->
 | 
			
		||||
                    stringProvider.getString(R.string.notice_display_name_changed_from,
 | 
			
		||||
                                             event.senderId, prevEventContent?.displayName, eventContent?.displayName)
 | 
			
		||||
                    sp.getString(R.string.notice_display_name_changed_from, event.senderId, prevEventContent?.displayName, eventContent?.displayName)
 | 
			
		||||
            }
 | 
			
		||||
            displayText.append(displayNameText)
 | 
			
		||||
        }
 | 
			
		||||
@ -163,73 +154,96 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
 | 
			
		||||
        if (eventContent?.avatarUrl != prevEventContent?.avatarUrl) {
 | 
			
		||||
            val displayAvatarText = if (displayText.isNotEmpty()) {
 | 
			
		||||
                displayText.append(" ")
 | 
			
		||||
                stringProvider.getString(R.string.notice_avatar_changed_too)
 | 
			
		||||
                sp.getString(R.string.notice_avatar_changed_too)
 | 
			
		||||
            } else {
 | 
			
		||||
                stringProvider.getString(R.string.notice_avatar_url_changed, senderName)
 | 
			
		||||
                sp.getString(R.string.notice_avatar_url_changed, senderName)
 | 
			
		||||
            }
 | 
			
		||||
            displayText.append(displayAvatarText)
 | 
			
		||||
        }
 | 
			
		||||
        if (displayText.isEmpty()) {
 | 
			
		||||
            displayText.append(
 | 
			
		||||
                    stringProvider.getString(R.string.notice_member_no_changes, senderName)
 | 
			
		||||
                    sp.getString(R.string.notice_member_no_changes, senderName)
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
        return displayText.toString()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun buildMembershipNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
 | 
			
		||||
        val senderDisplayName = senderName ?: event.senderId
 | 
			
		||||
        val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: ""
 | 
			
		||||
        return when {
 | 
			
		||||
            Membership.INVITE == eventContent?.membership -> {
 | 
			
		||||
        val senderDisplayName = senderName ?: event.senderId ?: ""
 | 
			
		||||
        val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
 | 
			
		||||
        return when (eventContent?.membership) {
 | 
			
		||||
            Membership.INVITE -> {
 | 
			
		||||
                val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId
 | 
			
		||||
                when {
 | 
			
		||||
                    eventContent.thirdPartyInvite != null -> {
 | 
			
		||||
                        val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid
 | 
			
		||||
                                                 ?: event.stateKey
 | 
			
		||||
                        stringProvider.getString(R.string.notice_room_third_party_registered_invite,
 | 
			
		||||
                                                 userWhoHasAccepted, eventContent.thirdPartyInvite?.displayName)
 | 
			
		||||
                        val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey
 | 
			
		||||
                        val threePidDisplayName = eventContent.thirdPartyInvite?.displayName ?: ""
 | 
			
		||||
                        eventContent.safeReason?.let { reason ->
 | 
			
		||||
                            sp.getString(R.string.notice_room_third_party_registered_invite_with_reason, userWhoHasAccepted, threePidDisplayName, reason)
 | 
			
		||||
                        } ?: sp.getString(R.string.notice_room_third_party_registered_invite, userWhoHasAccepted, threePidDisplayName)
 | 
			
		||||
                    }
 | 
			
		||||
                    event.stateKey == selfUserId          ->
 | 
			
		||||
                        stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
 | 
			
		||||
                        eventContent.safeReason?.let { reason ->
 | 
			
		||||
                            sp.getString(R.string.notice_room_invite_you_with_reason, senderDisplayName, reason)
 | 
			
		||||
                        } ?: sp.getString(R.string.notice_room_invite_you, senderDisplayName)
 | 
			
		||||
                    event.stateKey.isNullOrEmpty()        ->
 | 
			
		||||
                        stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
 | 
			
		||||
                        eventContent.safeReason?.let { reason ->
 | 
			
		||||
                            sp.getString(R.string.notice_room_invite_no_invitee_with_reason, senderDisplayName, reason)
 | 
			
		||||
                        } ?: sp.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
 | 
			
		||||
                    else                                  ->
 | 
			
		||||
                        stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
 | 
			
		||||
                        eventContent.safeReason?.let { reason ->
 | 
			
		||||
                            sp.getString(R.string.notice_room_invite_with_reason, senderDisplayName, targetDisplayName, reason)
 | 
			
		||||
                        } ?: sp.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            Membership.JOIN == eventContent?.membership   ->
 | 
			
		||||
                stringProvider.getString(R.string.notice_room_join, senderDisplayName)
 | 
			
		||||
            Membership.LEAVE == eventContent?.membership  ->
 | 
			
		||||
            Membership.JOIN   ->
 | 
			
		||||
                eventContent.safeReason?.let { reason ->
 | 
			
		||||
                    sp.getString(R.string.notice_room_join_with_reason, senderDisplayName, reason)
 | 
			
		||||
                } ?: sp.getString(R.string.notice_room_join, senderDisplayName)
 | 
			
		||||
            Membership.LEAVE  ->
 | 
			
		||||
                // 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
 | 
			
		||||
                return if (event.senderId == event.stateKey) {
 | 
			
		||||
                if (event.senderId == event.stateKey) {
 | 
			
		||||
                    if (prevEventContent?.membership == Membership.INVITE) {
 | 
			
		||||
                        stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
 | 
			
		||||
                        eventContent.safeReason?.let { reason ->
 | 
			
		||||
                            sp.getString(R.string.notice_room_reject_with_reason, senderDisplayName, reason)
 | 
			
		||||
                        } ?: sp.getString(R.string.notice_room_reject, senderDisplayName)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        stringProvider.getString(R.string.notice_room_leave, senderDisplayName)
 | 
			
		||||
                        eventContent.safeReason?.let { reason ->
 | 
			
		||||
                            sp.getString(R.string.notice_room_leave_with_reason, senderDisplayName, reason)
 | 
			
		||||
                        } ?: sp.getString(R.string.notice_room_leave, senderDisplayName)
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (prevEventContent?.membership == Membership.INVITE) {
 | 
			
		||||
                    stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
 | 
			
		||||
                    eventContent.safeReason?.let { reason ->
 | 
			
		||||
                        sp.getString(R.string.notice_room_withdraw_with_reason, senderDisplayName, targetDisplayName, reason)
 | 
			
		||||
                    } ?: sp.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
 | 
			
		||||
                } else if (prevEventContent?.membership == Membership.JOIN) {
 | 
			
		||||
                    stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
 | 
			
		||||
                    eventContent.safeReason?.let { reason ->
 | 
			
		||||
                        sp.getString(R.string.notice_room_kick_with_reason, senderDisplayName, targetDisplayName, reason)
 | 
			
		||||
                    } ?: sp.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
 | 
			
		||||
                } else if (prevEventContent?.membership == Membership.BAN) {
 | 
			
		||||
                    stringProvider.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName)
 | 
			
		||||
                    eventContent.safeReason?.let { reason ->
 | 
			
		||||
                        sp.getString(R.string.notice_room_unban_with_reason, senderDisplayName, targetDisplayName, reason)
 | 
			
		||||
                    } ?: sp.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName)
 | 
			
		||||
                } else {
 | 
			
		||||
                    null
 | 
			
		||||
                }
 | 
			
		||||
            Membership.BAN == eventContent?.membership    ->
 | 
			
		||||
                stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
 | 
			
		||||
            Membership.KNOCK == eventContent?.membership  ->
 | 
			
		||||
                stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
 | 
			
		||||
            else                                          -> null
 | 
			
		||||
            Membership.BAN    ->
 | 
			
		||||
                eventContent.safeReason?.let {
 | 
			
		||||
                    sp.getString(R.string.notice_room_ban_with_reason, senderDisplayName, targetDisplayName, it)
 | 
			
		||||
                } ?: sp.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
 | 
			
		||||
            Membership.KNOCK  ->
 | 
			
		||||
                eventContent.safeReason?.let { reason ->
 | 
			
		||||
                    sp.getString(R.string.notice_room_kick_with_reason, senderDisplayName, targetDisplayName, reason)
 | 
			
		||||
                } ?: sp.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
 | 
			
		||||
            else              -> null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun formatJoinRulesEvent(event: Event, senderName: String?): CharSequence? {
 | 
			
		||||
        val content = event.getClearContent().toModel<RoomJoinRulesContent>() ?: return null
 | 
			
		||||
        return when (content.joinRules) {
 | 
			
		||||
            RoomJoinRules.INVITE -> stringProvider.getString(R.string.room_join_rules_invite, senderName)
 | 
			
		||||
            RoomJoinRules.PUBLIC -> stringProvider.getString(R.string.room_join_rules_public, senderName)
 | 
			
		||||
            RoomJoinRules.INVITE -> sp.getString(R.string.room_join_rules_invite, senderName)
 | 
			
		||||
            RoomJoinRules.PUBLIC -> sp.getString(R.string.room_join_rules_public, senderName)
 | 
			
		||||
            else                 -> null
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -25,12 +25,14 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
 | 
			
		||||
import im.vector.matrix.android.api.session.room.send.SendState
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.di.ActiveSessionHolder
 | 
			
		||||
import im.vector.riotx.core.di.ScreenScope
 | 
			
		||||
import im.vector.riotx.core.error.ErrorFormatter
 | 
			
		||||
import im.vector.riotx.core.resources.ColorProvider
 | 
			
		||||
import im.vector.riotx.core.utils.TextUtils
 | 
			
		||||
import im.vector.riotx.features.ui.getMessageTextColor
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
@ScreenScope
 | 
			
		||||
class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
 | 
			
		||||
                                                          private val colorProvider: ColorProvider,
 | 
			
		||||
                                                          private val errorFormatter: ErrorFormatter) {
 | 
			
		||||
@ -40,7 +42,7 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
 | 
			
		||||
    fun bind(eventId: String,
 | 
			
		||||
             isLocalFile: Boolean,
 | 
			
		||||
             progressLayout: ViewGroup) {
 | 
			
		||||
        activeSessionHolder.getActiveSession().also { session ->
 | 
			
		||||
        activeSessionHolder.getSafeActiveSession()?.also { session ->
 | 
			
		||||
            val uploadStateTracker = session.contentUploadProgressTracker()
 | 
			
		||||
            val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter)
 | 
			
		||||
            updateListeners[eventId] = updateListener
 | 
			
		||||
@ -49,13 +51,19 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun unbind(eventId: String) {
 | 
			
		||||
        activeSessionHolder.getActiveSession().also { session ->
 | 
			
		||||
        activeSessionHolder.getSafeActiveSession()?.also { session ->
 | 
			
		||||
            val uploadStateTracker = session.contentUploadProgressTracker()
 | 
			
		||||
            updateListeners[eventId]?.also {
 | 
			
		||||
                uploadStateTracker.untrack(eventId, it)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun clear() {
 | 
			
		||||
        activeSessionHolder.getSafeActiveSession()?.also {
 | 
			
		||||
            it.contentUploadProgressTracker().clear()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
 | 
			
		||||
 | 
			
		||||
@ -19,11 +19,12 @@ package im.vector.riotx.features.home.room.detail.timeline.helper
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import im.vector.riotx.core.di.ScreenScope
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
import kotlin.math.roundToInt
 | 
			
		||||
 | 
			
		||||
@ScreenScope
 | 
			
		||||
class TimelineMediaSizeProvider @Inject constructor() {
 | 
			
		||||
 | 
			
		||||
    lateinit var recyclerView: RecyclerView
 | 
			
		||||
    var recyclerView: RecyclerView? = null
 | 
			
		||||
    private var cachedSize: Pair<Int, Int>? = null
 | 
			
		||||
 | 
			
		||||
    fun getMaxSize(): Pair<Int, Int> {
 | 
			
		||||
@ -31,17 +32,17 @@ class TimelineMediaSizeProvider @Inject constructor() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun computeMaxSize(): Pair<Int, Int> {
 | 
			
		||||
        val width = recyclerView.width
 | 
			
		||||
        val height = recyclerView.height
 | 
			
		||||
        val width = recyclerView?.width ?: 0
 | 
			
		||||
        val height = recyclerView?.height ?: 0
 | 
			
		||||
        val maxImageWidth: Int
 | 
			
		||||
        val maxImageHeight: Int
 | 
			
		||||
        // landscape / portrait
 | 
			
		||||
        if (width < height) {
 | 
			
		||||
            maxImageWidth = Math.round(width * 0.7f)
 | 
			
		||||
            maxImageHeight = Math.round(height * 0.5f)
 | 
			
		||||
            maxImageWidth = (width * 0.7f).roundToInt()
 | 
			
		||||
            maxImageHeight = (height * 0.5f).roundToInt()
 | 
			
		||||
        } else {
 | 
			
		||||
            maxImageWidth = Math.round(width * 0.5f)
 | 
			
		||||
            maxImageHeight = Math.round(height * 0.7f)
 | 
			
		||||
            maxImageWidth = (width * 0.5f).roundToInt()
 | 
			
		||||
            maxImageHeight = (height * 0.7f).roundToInt()
 | 
			
		||||
        }
 | 
			
		||||
        return Pair(maxImageWidth, maxImageHeight)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,6 @@ import android.os.Bundle
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import butterknife.BindView
 | 
			
		||||
import butterknife.ButterKnife
 | 
			
		||||
@ -29,6 +28,8 @@ import com.airbnb.mvrx.fragmentViewModel
 | 
			
		||||
import com.airbnb.mvrx.withState
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.di.ScreenComponent
 | 
			
		||||
import im.vector.riotx.core.extensions.cleanup
 | 
			
		||||
import im.vector.riotx.core.extensions.configureWith
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
 | 
			
		||||
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
 | 
			
		||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
 | 
			
		||||
@ -49,8 +50,8 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
 | 
			
		||||
 | 
			
		||||
    @Inject lateinit var epoxyController: ViewReactionsEpoxyController
 | 
			
		||||
 | 
			
		||||
    override fun injectWith(screenComponent: ScreenComponent) {
 | 
			
		||||
        screenComponent.inject(this)
 | 
			
		||||
    override fun injectWith(injector: ScreenComponent) {
 | 
			
		||||
        injector.inject(this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
 | 
			
		||||
@ -61,11 +62,15 @@ class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
 | 
			
		||||
 | 
			
		||||
    override fun onActivityCreated(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onActivityCreated(savedInstanceState)
 | 
			
		||||
        recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
 | 
			
		||||
        recyclerView.adapter = epoxyController.adapter
 | 
			
		||||
        recyclerView.configureWith(epoxyController, hasFixedSize = false)
 | 
			
		||||
        bottomSheetTitle.text = context?.getString(R.string.reactions)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        recyclerView.cleanup()
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun invalidate() = withState(viewModel) {
 | 
			
		||||
        epoxyController.setData(it)
 | 
			
		||||
        super.invalidate()
 | 
			
		||||
 | 
			
		||||
@ -36,9 +36,7 @@ class FilteredRoomsActivity : VectorBaseActivity() {
 | 
			
		||||
            return supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? RoomListFragment
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int {
 | 
			
		||||
        return R.layout.activity_filtered_rooms
 | 
			
		||||
    }
 | 
			
		||||
    override fun getLayoutRes() = R.layout.activity_filtered_rooms
 | 
			
		||||
 | 
			
		||||
    override fun injectWith(injector: ScreenComponent) {
 | 
			
		||||
        injector.inject(this)
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,7 @@ import androidx.core.content.ContextCompat
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import com.airbnb.epoxy.OnModelBuildFinishedListener
 | 
			
		||||
import com.airbnb.mvrx.*
 | 
			
		||||
import com.google.android.material.snackbar.Snackbar
 | 
			
		||||
import im.vector.matrix.android.api.failure.Failure
 | 
			
		||||
@ -35,13 +36,13 @@ import im.vector.matrix.android.api.session.room.notification.RoomNotificationSt
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
 | 
			
		||||
import im.vector.riotx.core.error.ErrorFormatter
 | 
			
		||||
import im.vector.riotx.core.extensions.cleanup
 | 
			
		||||
import im.vector.riotx.core.platform.OnBackPressed
 | 
			
		||||
import im.vector.riotx.core.platform.StateView
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseFragment
 | 
			
		||||
 | 
			
		||||
import im.vector.riotx.features.home.RoomListDisplayMode
 | 
			
		||||
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedAction
 | 
			
		||||
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsBottomSheet
 | 
			
		||||
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedAction
 | 
			
		||||
import im.vector.riotx.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
 | 
			
		||||
import im.vector.riotx.features.home.room.list.widget.FabMenuView
 | 
			
		||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
 | 
			
		||||
@ -65,6 +66,7 @@ class RoomListFragment @Inject constructor(
 | 
			
		||||
 | 
			
		||||
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
 | 
			
		||||
 | 
			
		||||
    private var modelBuildListener: OnModelBuildFinishedListener? = null
 | 
			
		||||
    private lateinit var sharedActionViewModel: RoomListQuickActionsSharedActionViewModel
 | 
			
		||||
    private val roomListParams: RoomListParams by args()
 | 
			
		||||
    private val roomListViewModel: RoomListViewModel by fragmentViewModel()
 | 
			
		||||
@ -118,8 +120,12 @@ class RoomListFragment @Inject constructor(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        roomController.removeModelBuildListener(modelBuildListener)
 | 
			
		||||
        modelBuildListener = null
 | 
			
		||||
        roomListView.cleanup()
 | 
			
		||||
        roomController.listener = null
 | 
			
		||||
        createChatFabMenu.listener = null
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
        roomListView.adapter = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) {
 | 
			
		||||
@ -198,7 +204,8 @@ class RoomListFragment @Inject constructor(
 | 
			
		||||
        roomListView.layoutManager = layoutManager
 | 
			
		||||
        roomListView.itemAnimator = RoomListAnimator()
 | 
			
		||||
        roomController.listener = this
 | 
			
		||||
        roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
 | 
			
		||||
        modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) }
 | 
			
		||||
        roomController.addModelBuildListener(modelBuildListener)
 | 
			
		||||
        roomListView.adapter = roomController.adapter
 | 
			
		||||
        stateView.contentView = roomListView
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
                // We do not update the joiningRoomsIds here, because, the room is not joined yet regarding the sync data.
 | 
			
		||||
                // Instead, we wait for the room to be joined
 | 
			
		||||
@ -158,7 +158,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        session.getRoom(roomId)?.leave(object : MatrixCallback<Unit> {
 | 
			
		||||
        session.getRoom(roomId)?.leave(null, object : MatrixCallback<Unit> {
 | 
			
		||||
            override fun onSuccess(data: Unit) {
 | 
			
		||||
                // We do not update the rejectingRoomsIds here, because, the room is not rejected yet regarding the sync data.
 | 
			
		||||
                // Instead, we wait for the room to be rejected
 | 
			
		||||
@ -197,7 +197,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleLeaveRoom(action: RoomListAction.LeaveRoom) {
 | 
			
		||||
        session.getRoom(action.roomId)?.leave(object : MatrixCallback<Unit> {
 | 
			
		||||
        session.getRoom(action.roomId)?.leave(null, object : MatrixCallback<Unit> {
 | 
			
		||||
            override fun onFailure(failure: Throwable) {
 | 
			
		||||
                _viewEvents.post(RoomListViewEvents.Failure(failure))
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -42,7 +42,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        // We are requesting a model build directly as the first build of epoxy is on the main thread.
 | 
			
		||||
        // It avoids to build the the whole list of rooms on the main thread.
 | 
			
		||||
        // It avoids to build the whole list of rooms on the main thread.
 | 
			
		||||
        requestModelBuild()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,6 @@ import android.os.Parcelable
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import butterknife.BindView
 | 
			
		||||
import butterknife.ButterKnife
 | 
			
		||||
@ -29,6 +28,8 @@ import com.airbnb.mvrx.fragmentViewModel
 | 
			
		||||
import com.airbnb.mvrx.withState
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.di.ScreenComponent
 | 
			
		||||
import im.vector.riotx.core.extensions.cleanup
 | 
			
		||||
import im.vector.riotx.core.extensions.configureWith
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
 | 
			
		||||
import im.vector.riotx.features.navigation.Navigator
 | 
			
		||||
import kotlinx.android.parcel.Parcelize
 | 
			
		||||
@ -56,8 +57,8 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R
 | 
			
		||||
 | 
			
		||||
    override val showExpanded = true
 | 
			
		||||
 | 
			
		||||
    override fun injectWith(screenComponent: ScreenComponent) {
 | 
			
		||||
        screenComponent.inject(this)
 | 
			
		||||
    override fun injectWith(injector: ScreenComponent) {
 | 
			
		||||
        injector.inject(this)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
 | 
			
		||||
@ -69,13 +70,17 @@ class RoomListQuickActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), R
 | 
			
		||||
    override fun onActivityCreated(savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onActivityCreated(savedInstanceState)
 | 
			
		||||
        sharedActionViewModel = activityViewModelProvider.get(RoomListQuickActionsSharedActionViewModel::class.java)
 | 
			
		||||
        recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
 | 
			
		||||
        recyclerView.adapter = roomListActionsEpoxyController.adapter
 | 
			
		||||
        recyclerView.configureWith(roomListActionsEpoxyController, hasFixedSize = false)
 | 
			
		||||
        // Disable item animation
 | 
			
		||||
        recyclerView.itemAnimator = null
 | 
			
		||||
        roomListActionsEpoxyController.listener = this
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        recyclerView.cleanup()
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun invalidate() = withState(viewModel) {
 | 
			
		||||
        roomListActionsEpoxyController.setData(it)
 | 
			
		||||
        super.invalidate()
 | 
			
		||||
 | 
			
		||||
@ -539,7 +539,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
 | 
			
		||||
                                setState {
 | 
			
		||||
                                    copy(
 | 
			
		||||
                                            asyncHomeServerLoginFlowRequest = Uninitialized,
 | 
			
		||||
                                            homeServerUrl = action.homeServerUrl,
 | 
			
		||||
                                            homeServerUrl = data.homeServerUrl,
 | 
			
		||||
                                            loginMode = loginMode,
 | 
			
		||||
                                            loginModeSupportedTypes = data.loginFlowResponse.flows.mapNotNull { it.type }.toList()
 | 
			
		||||
                                    )
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,8 @@ import butterknife.OnClick
 | 
			
		||||
import com.airbnb.mvrx.args
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.error.ErrorFormatter
 | 
			
		||||
import im.vector.riotx.core.extensions.cleanup
 | 
			
		||||
import im.vector.riotx.core.extensions.configureWith
 | 
			
		||||
import im.vector.riotx.core.utils.openUrlInExternalBrowser
 | 
			
		||||
import im.vector.riotx.features.login.AbstractLoginFragment
 | 
			
		||||
import im.vector.riotx.features.login.LoginAction
 | 
			
		||||
@ -55,8 +57,7 @@ class LoginTermsFragment @Inject constructor(
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        loginTermsPolicyList.setController(policyController)
 | 
			
		||||
        loginTermsPolicyList.configureWith(policyController)
 | 
			
		||||
        policyController.listener = this
 | 
			
		||||
 | 
			
		||||
        val list = ArrayList<LocalizedFlowDataLoginTermsChecked>()
 | 
			
		||||
@ -69,6 +70,12 @@ class LoginTermsFragment @Inject constructor(
 | 
			
		||||
        loginTermsViewState = LoginTermsViewState(list)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        loginTermsPolicyList.cleanup()
 | 
			
		||||
        policyController.listener = null
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun renderState() {
 | 
			
		||||
        policyController.setData(loginTermsViewState.localizedFlowDataLoginTermsChecked)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -74,14 +74,14 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
 | 
			
		||||
    private fun handleJoinRoom(roomId: String) {
 | 
			
		||||
        activeSessionHolder.getSafeActiveSession()?.let { session ->
 | 
			
		||||
            session.getRoom(roomId)
 | 
			
		||||
                    ?.join(emptyList(), object : MatrixCallback<Unit> {})
 | 
			
		||||
                    ?.join(callback = object : MatrixCallback<Unit> {})
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun handleRejectRoom(roomId: String) {
 | 
			
		||||
        activeSessionHolder.getSafeActiveSession()?.let { session ->
 | 
			
		||||
            session.getRoom(roomId)
 | 
			
		||||
                    ?.leave(object : MatrixCallback<Unit> {})
 | 
			
		||||
                    ?.leave(callback = object : MatrixCallback<Unit> {})
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,12 +17,18 @@ package im.vector.riotx.features.reactions
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.View
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import androidx.lifecycle.observe
 | 
			
		||||
import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.extensions.cleanup
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseFragment
 | 
			
		||||
import kotlinx.android.synthetic.main.emoji_chooser_fragment.*
 | 
			
		||||
import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
class EmojiChooserFragment @Inject constructor() : VectorBaseFragment() {
 | 
			
		||||
class EmojiChooserFragment @Inject constructor(
 | 
			
		||||
        private val emojiRecyclerAdapter: EmojiRecyclerAdapter
 | 
			
		||||
) : VectorBaseFragment(),
 | 
			
		||||
        EmojiRecyclerAdapter.InteractionListener,
 | 
			
		||||
        ReactionClickListener {
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutResId() = R.layout.emoji_chooser_fragment
 | 
			
		||||
 | 
			
		||||
@ -31,10 +37,29 @@ class EmojiChooserFragment @Inject constructor() : VectorBaseFragment() {
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
        viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java)
 | 
			
		||||
        viewModel.initWithContext(context!!)
 | 
			
		||||
        (view as? RecyclerView)?.let {
 | 
			
		||||
            it.adapter = viewModel.adapter
 | 
			
		||||
            it.adapter?.notifyDataSetChanged()
 | 
			
		||||
 | 
			
		||||
        emojiRecyclerAdapter.reactionClickListener = this
 | 
			
		||||
        emojiRecyclerAdapter.interactionListener = this
 | 
			
		||||
 | 
			
		||||
        emojiRecyclerView.adapter = emojiRecyclerAdapter
 | 
			
		||||
 | 
			
		||||
        viewModel.moveToSection.observe(viewLifecycleOwner) { section ->
 | 
			
		||||
            emojiRecyclerAdapter.scrollToSection(section)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun firstVisibleSectionChange(section: Int) {
 | 
			
		||||
        viewModel.setCurrentSection(section)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onReactionSelected(reaction: String) {
 | 
			
		||||
        viewModel.onReactionSelected(reaction)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroyView() {
 | 
			
		||||
        emojiRecyclerView.cleanup()
 | 
			
		||||
        emojiRecyclerAdapter.reactionClickListener = null
 | 
			
		||||
        emojiRecyclerAdapter.interactionListener = null
 | 
			
		||||
        super.onDestroyView()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,6 @@
 | 
			
		||||
 */
 | 
			
		||||
package im.vector.riotx.features.reactions
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import androidx.lifecycle.MutableLiveData
 | 
			
		||||
import androidx.lifecycle.ViewModel
 | 
			
		||||
import im.vector.riotx.core.utils.LiveEvent
 | 
			
		||||
@ -23,36 +22,26 @@ import javax.inject.Inject
 | 
			
		||||
 | 
			
		||||
class EmojiChooserViewModel @Inject constructor() : ViewModel() {
 | 
			
		||||
 | 
			
		||||
    var adapter: EmojiRecyclerAdapter? = null
 | 
			
		||||
    val emojiSourceLiveData: MutableLiveData<EmojiDataSource> = MutableLiveData()
 | 
			
		||||
 | 
			
		||||
    val navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()
 | 
			
		||||
    var selectedReaction: String? = null
 | 
			
		||||
    var eventId: String? = null
 | 
			
		||||
 | 
			
		||||
    val currentSection: MutableLiveData<Int> = MutableLiveData()
 | 
			
		||||
    val moveToSection: MutableLiveData<Int> = MutableLiveData()
 | 
			
		||||
 | 
			
		||||
    var reactionClickListener = object : ReactionClickListener {
 | 
			
		||||
        override fun onReactionSelected(reaction: String) {
 | 
			
		||||
            selectedReaction = reaction
 | 
			
		||||
            navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
 | 
			
		||||
        }
 | 
			
		||||
    fun onReactionSelected(reaction: String) {
 | 
			
		||||
        selectedReaction = reaction
 | 
			
		||||
        navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun initWithContext(context: Context) {
 | 
			
		||||
        // TODO load async
 | 
			
		||||
        val emojiDataSource = EmojiDataSource(context)
 | 
			
		||||
        emojiSourceLiveData.value = emojiDataSource
 | 
			
		||||
        adapter = EmojiRecyclerAdapter(emojiDataSource, reactionClickListener)
 | 
			
		||||
        adapter?.interactionListener = object : EmojiRecyclerAdapter.InteractionListener {
 | 
			
		||||
            override fun firstVisibleSectionChange(section: Int) {
 | 
			
		||||
                currentSection.value = section
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    // Called by the Fragment, when the List is scrolled
 | 
			
		||||
    fun setCurrentSection(section: Int) {
 | 
			
		||||
        currentSection.value = section
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun scrollToSection(sectionIndex: Int) {
 | 
			
		||||
        adapter?.scrollToSection(sectionIndex)
 | 
			
		||||
    // Called by the Activity, when a tab item is clicked
 | 
			
		||||
    fun scrollToSection(section: Int) {
 | 
			
		||||
        moveToSection.value = section
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
}
 | 
			
		||||
@ -35,6 +35,7 @@ import im.vector.riotx.R
 | 
			
		||||
import im.vector.riotx.core.di.ScreenComponent
 | 
			
		||||
import im.vector.riotx.core.extensions.observeEvent
 | 
			
		||||
import im.vector.riotx.core.platform.VectorBaseActivity
 | 
			
		||||
import im.vector.riotx.features.reactions.data.EmojiDataSource
 | 
			
		||||
import io.reactivex.android.schedulers.AndroidSchedulers
 | 
			
		||||
import kotlinx.android.synthetic.main.activity_emoji_reaction_picker.*
 | 
			
		||||
import timber.log.Timber
 | 
			
		||||
@ -44,7 +45,6 @@ import javax.inject.Inject
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * TODO: Loading indicator while getting emoji data source?
 | 
			
		||||
 * TODO: migrate to MvRx
 | 
			
		||||
 * TODO: Finish Refactor to vector base activity
 | 
			
		||||
 */
 | 
			
		||||
class EmojiReactionPickerActivity : VectorBaseActivity(),
 | 
			
		||||
@ -54,13 +54,15 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
 | 
			
		||||
 | 
			
		||||
    lateinit var viewModel: EmojiChooserViewModel
 | 
			
		||||
 | 
			
		||||
    override fun getMenuRes(): Int = R.menu.menu_emoji_reaction_picker
 | 
			
		||||
    override fun getMenuRes() = R.menu.menu_emoji_reaction_picker
 | 
			
		||||
 | 
			
		||||
    override fun getLayoutRes(): Int = R.layout.activity_emoji_reaction_picker
 | 
			
		||||
    override fun getLayoutRes() = R.layout.activity_emoji_reaction_picker
 | 
			
		||||
 | 
			
		||||
    override fun getTitleRes(): Int = R.string.title_activity_emoji_reaction_picker
 | 
			
		||||
    override fun getTitleRes() = R.string.title_activity_emoji_reaction_picker
 | 
			
		||||
 | 
			
		||||
    @Inject lateinit var emojiSearchResultViewModelFactory: EmojiSearchResultViewModel.Factory
 | 
			
		||||
    @Inject lateinit var emojiCompatFontProvider: EmojiCompatFontProvider
 | 
			
		||||
    @Inject lateinit var emojiDataSource: EmojiDataSource
 | 
			
		||||
 | 
			
		||||
    private val searchResultViewModel: EmojiSearchResultViewModel by viewModel()
 | 
			
		||||
 | 
			
		||||
@ -93,22 +95,18 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
 | 
			
		||||
 | 
			
		||||
        viewModel.eventId = intent.getStringExtra(EXTRA_EVENT_ID)
 | 
			
		||||
 | 
			
		||||
        viewModel.emojiSourceLiveData.observe(this, Observer {
 | 
			
		||||
            it.rawData?.categories?.let { categories ->
 | 
			
		||||
                for (category in categories) {
 | 
			
		||||
                    val s = category.emojis[0]
 | 
			
		||||
                    tabLayout.newTab()
 | 
			
		||||
                            .also { tab ->
 | 
			
		||||
                                tab.text = it.rawData!!.emojis[s]!!.emojiString()
 | 
			
		||||
                                tab.contentDescription = category.name
 | 
			
		||||
                            }
 | 
			
		||||
                            .also { tab ->
 | 
			
		||||
                                tabLayout.addTab(tab)
 | 
			
		||||
                            }
 | 
			
		||||
                }
 | 
			
		||||
                tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener)
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        emojiDataSource.rawData.categories.forEach { category ->
 | 
			
		||||
            val s = category.emojis[0]
 | 
			
		||||
            tabLayout.newTab()
 | 
			
		||||
                    .also { tab ->
 | 
			
		||||
                        tab.text = emojiDataSource.rawData.emojis[s]!!.emoji
 | 
			
		||||
                        tab.contentDescription = category.name
 | 
			
		||||
                    }
 | 
			
		||||
                    .also { tab ->
 | 
			
		||||
                        tabLayout.addTab(tab)
 | 
			
		||||
                    }
 | 
			
		||||
        }
 | 
			
		||||
        tabLayout.addOnTabSelectedListener(tabLayoutSelectionListener)
 | 
			
		||||
 | 
			
		||||
        viewModel.currentSection.observe(this, Observer { section ->
 | 
			
		||||
            section?.let {
 | 
			
		||||
@ -136,7 +134,6 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
 | 
			
		||||
 | 
			
		||||
    override fun compatibilityFontUpdate(typeface: Typeface?) {
 | 
			
		||||
        EmojiDrawView.configureTextPaint(this, typeface)
 | 
			
		||||
        searchResultViewModel.dataSource
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDestroy() {
 | 
			
		||||
@ -206,13 +203,19 @@ class EmojiReactionPickerActivity : VectorBaseActivity(),
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
 | 
			
		||||
        const val EXTRA_EVENT_ID = "EXTRA_EVENT_ID"
 | 
			
		||||
        const val EXTRA_REACTION_RESULT = "EXTRA_REACTION_RESULT"
 | 
			
		||||
        private const val EXTRA_EVENT_ID = "EXTRA_EVENT_ID"
 | 
			
		||||
        private const val EXTRA_REACTION_RESULT = "EXTRA_REACTION_RESULT"
 | 
			
		||||
 | 
			
		||||
        fun intent(context: Context, eventId: String): Intent {
 | 
			
		||||
            val intent = Intent(context, EmojiReactionPickerActivity::class.java)
 | 
			
		||||
            intent.putExtra(EXTRA_EVENT_ID, eventId)
 | 
			
		||||
            return intent
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun getOutput(data: Intent): Pair<String, String>? {
 | 
			
		||||
            val eventId = data.getStringExtra(EXTRA_EVENT_ID) ?: return null
 | 
			
		||||
            val reaction = data.getStringExtra(EXTRA_REACTION_RESULT) ?: return null
 | 
			
		||||
            return eventId to reaction
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user