mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-01-10 10:17:42 +03:00
Merge branch 'release/0.10.0'
This commit is contained in:
commit
902a9aa243
137 changed files with 2083 additions and 674 deletions
17
CHANGES.md
17
CHANGES.md
|
@ -1,3 +1,20 @@
|
|||
Changes in RiotX 0.10.0 (2019-12-10)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Breadcrumbs: switch from one room to another quickly (#571)
|
||||
|
||||
Improvements 🙌:
|
||||
- Support entering a RiotWeb client URL instead of the homeserver URL during connection (#744)
|
||||
|
||||
Other changes:
|
||||
- Add reason for all membership events (https://github.com/matrix-org/matrix-doc/pull/2367)
|
||||
|
||||
Bugfix 🐛:
|
||||
- When automardown is ON, pills are sent as MD in body (#739)
|
||||
- "ban" event are not rendered correctly (#716)
|
||||
- Fix crash when rotating screen in Room timeline
|
||||
|
||||
Changes in RiotX 0.9.1 (2019-12-05)
|
||||
===================================================
|
||||
|
||||
|
|
|
@ -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
|
||||
val versions = executeRequest<Versions> {
|
||||
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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (versions.isSupportedBySdk()) {
|
||||
private suspend fun getRiotLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig): LoginFlowResult {
|
||||
val authAPI = buildAuthAPI(homeServerConnectionConfig)
|
||||
|
||||
// Ok, try to get the config.json file of a RiotWeb client
|
||||
val riotConfig = executeRequest<RiotConfig> {
|
||||
apiCall = authAPI.getRiotConfig()
|
||||
}
|
||||
|
||||
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 {
|
||||
?: 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,7 +45,8 @@ 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 {
|
||||
?: realm.createObject<ReadReceiptEntity>(buildPrimaryKey(roomId, userId))
|
||||
.apply {
|
||||
this.roomId = roomId
|
||||
this.userId = 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 {
|
||||
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)
|
||||
return CancelableWork(context, sendRelationWork.id)
|
||||
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
|
||||
|
|
|
@ -289,6 +289,9 @@ internal class DefaultTimeline(
|
|||
}
|
||||
|
||||
override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
|
||||
if (listeners.contains(listener)) {
|
||||
return false
|
||||
}
|
||||
listeners.add(listener).also {
|
||||
postSnapshot()
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -82,3 +82,6 @@ layout_constraintLeft_
|
|||
### Will crash on API < 21. Use ?colorAccent instead
|
||||
\?android:colorAccent
|
||||
\?android:attr/colorAccent
|
||||
|
||||
### Use androidx.recyclerview.widget.RecyclerView because EpoxyRecyclerViews add behavior we do not want to
|
||||
<com\.airbnb\.epoxy\.EpoxyRecyclerView
|
||||
|
|
|
@ -15,8 +15,8 @@ androidExtensions {
|
|||
}
|
||||
|
||||
ext.versionMajor = 0
|
||||
ext.versionMinor = 9
|
||||
ext.versionPatch = 1
|
||||
ext.versionMinor = 10
|
||||
ext.versionPatch = 0
|
||||
|
||||
static def getGitTimestamp() {
|
||||
def cmd = 'git show -s --format=%ct'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -135,6 +135,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
|
|||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
restorables.forEach { it.onSaveInstanceState(outState) }
|
||||
restorables.clear()
|
||||
}
|
||||
|
||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||
|
|
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)
|
||||
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)
|
||||
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
|
||||
|
@ -193,6 +191,8 @@ class RoomDetailFragment @Inject constructor(
|
|||
|
||||
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
||||
private lateinit var layoutManager: LinearLayoutManager
|
||||
private var modelBuildListener: OnModelBuildFinishedListener? = null
|
||||
|
||||
private lateinit var attachmentsHelper: AttachmentsHelper
|
||||
private lateinit var keyboardStateUtils: KeyboardStateUtils
|
||||
|
||||
|
@ -286,13 +286,16 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
timelineEventController.callback = null
|
||||
timelineEventController.removeModelBuildListener(modelBuildListener)
|
||||
modelBuildListener = null
|
||||
debouncer.cancelAll()
|
||||
recyclerView.cleanup()
|
||||
super.onDestroyView()
|
||||
recyclerView.adapter = null
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
|
||||
debouncer.cancelAll()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
@ -447,11 +450,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
|
||||
when (requestCode) {
|
||||
REACTION_SELECT_REQUEST_CODE -> {
|
||||
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
||||
?: return
|
||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
||||
?: return
|
||||
// TODO check if already reacted with that?
|
||||
val (eventId, reaction) = EmojiReactionPickerActivity.getOutput(data) ?: return
|
||||
roomDetailViewModel.handle(RoomDetailAction.SendReaction(eventId, reaction))
|
||||
}
|
||||
}
|
||||
|
@ -470,13 +469,14 @@ class RoomDetailFragment @Inject constructor(
|
|||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.itemAnimator = null
|
||||
recyclerView.setHasFixedSize(true)
|
||||
timelineEventController.addModelBuildListener {
|
||||
modelBuildListener = OnModelBuildFinishedListener {
|
||||
it.dispatchTo(stateRestorer)
|
||||
it.dispatchTo(scrollOnNewMessageCallback)
|
||||
it.dispatchTo(scrollOnHighlightedEventCallback)
|
||||
updateJumpToReadMarkerViewVisibility()
|
||||
updateJumpToBottomViewVisibility()
|
||||
}
|
||||
timelineEventController.addModelBuildListener(modelBuildListener)
|
||||
recyclerView.adapter = timelineEventController.adapter
|
||||
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
|
@ -521,7 +521,8 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateJumpToReadMarkerViewVisibility() = jumpToReadMarkerView.post {
|
||||
private fun updateJumpToReadMarkerViewVisibility() {
|
||||
jumpToReadMarkerView?.post {
|
||||
withState(roomDetailViewModel) {
|
||||
val showJumpToUnreadBanner = when (it.unreadState) {
|
||||
UnreadState.Unknown,
|
||||
|
@ -544,6 +545,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateJumpToBottomViewVisibility() {
|
||||
debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {
|
||||
|
@ -1181,7 +1183,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
&& userId == session.myUserId) {
|
||||
// Empty composer, current user: start an emote
|
||||
composerLayout.composerEditText.setText(Command.EMOTE.command + " ")
|
||||
composerLayout.composerEditText.setSelection(Command.EMOTE.command.length + 1)
|
||||
composerLayout.composerEditText.setSelection(Command.EMOTE.length)
|
||||
} else {
|
||||
val roomMember = roomDetailViewModel.getMember(userId)
|
||||
// TODO move logic outside of fragment
|
||||
|
|
|
@ -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>()
|
|
@ -143,6 +143,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
timeline.addListener(this)
|
||||
timeline.start()
|
||||
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
||||
|
||||
// Inform the SDK that the room is displayed
|
||||
session.onRoomDisplayed(initialState.roomId)
|
||||
}
|
||||
|
||||
override fun handle(action: RoomDetailAction) {
|
||||
|
@ -197,9 +200,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
invisibleEventsObservable.accept(action)
|
||||
}
|
||||
|
||||
fun getMember(userId: String) : RoomMember? {
|
||||
fun getMember(userId: String): RoomMember? {
|
||||
return room.getRoomMember(userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a send mode to a draft and save the draft
|
||||
*/
|
||||
|
@ -263,7 +267,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
}
|
||||
}
|
||||
session.rx()
|
||||
.joinRoom(roomId, viaServer)
|
||||
.joinRoom(roomId, viaServers = viaServer)
|
||||
.map { roomId }
|
||||
.execute {
|
||||
copy(tombstoneEventHandling = it)
|
||||
|
@ -484,7 +488,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
private fun handleInviteSlashCommand(invite: ParsedCommand.Invite) {
|
||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
|
||||
|
||||
room.invite(invite.userId, object : MatrixCallback<Unit> {
|
||||
room.invite(invite.userId, invite.reason, object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandResultOk)
|
||||
}
|
||||
|
@ -550,7 +554,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
}
|
||||
|
||||
private fun handleRejectInvite() {
|
||||
room.leave(object : MatrixCallback<Unit> {})
|
||||
room.leave(null, object : MatrixCallback<Unit> {})
|
||||
}
|
||||
|
||||
private fun handleAcceptInvite() {
|
||||
|
@ -859,7 +863,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
|
||||
override fun onCleared() {
|
||||
timeline.dispose()
|
||||
timeline.removeAllListeners()
|
||||
timeline.removeListener(this)
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,64 +154,87 @@ 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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -228,8 +242,8 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
|||
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) {
|
||||
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) {
|
||||
// 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',
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue