Merge branch 'release/0.10.0'

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

View file

@ -1,3 +1,20 @@
Changes in RiotX 0.10.0 (2019-12-10)
===================================================
Features ✨:
- Breadcrumbs: switch from one room to another quickly (#571)
Improvements 🙌:
- Support entering a RiotWeb client URL instead of the homeserver URL during connection (#744)
Other changes:
- Add reason for all membership events (https://github.com/matrix-org/matrix-doc/pull/2367)
Bugfix 🐛:
- When automardown is ON, pills are sent as MD in body (#739)
- "ban" event are not rendered correctly (#716)
- Fix crash when rotating screen in Room timeline
Changes in RiotX 0.9.1 (2019-12-05)
===================================================

View file

@ -17,7 +17,7 @@ Client request the sign-up flows, once the homeserver is chosen by the user and
}
```
We get the flows with a 401, which also means the the registration is possible on this homeserver.
We get the flows with a 401, which also means that the registration is possible on this homeserver.
```json
{

View file

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

View file

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

View file

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

View file

@ -36,7 +36,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
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()))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)
*/

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.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
*/

View file

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

View file

@ -0,0 +1,28 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class RiotConfig(
// There are plenty of other elements in the file config.json of a RiotWeb client, but for the moment only one is interesting
// Ex: "brand", "branding", etc.
@Json(name = "default_hs_url")
val defaultHomeServerUrl: String?
)

View file

@ -19,16 +19,14 @@ package im.vector.matrix.android.internal.crypto.store.db.query
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity
import im.vector.matrix.android.internal.crypto.store.db.model.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)
}
/**

View file

@ -20,18 +20,20 @@ import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.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
}
}
}

View file

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto.store.db.query
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
import im.vector.matrix.android.internal.crypto.store.db.model.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)
}
/**

View file

@ -0,0 +1,27 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model
import io.realm.RealmList
import io.realm.RealmObject
internal open class BreadcrumbsEntity(
var recentRoomIds: RealmList<String> = RealmList()
) : RealmObject() {
companion object
}

View file

@ -38,7 +38,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var readMarkerId: String? = null,
var 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
}
}

View file

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

View file

@ -0,0 +1,30 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
internal fun BreadcrumbsEntity.Companion.get(realm: Realm): BreadcrumbsEntity? {
return realm.where<BreadcrumbsEntity>().findFirst()
}
internal fun BreadcrumbsEntity.Companion.getOrCreate(realm: Realm): BreadcrumbsEntity {
return get(realm) ?: realm.createObject()
}

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
import 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)
}

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntityFields
import 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
}

View file

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

View file

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory
import im.vector.matrix.android.internal.network.parsing.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)

View file

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

View file

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

View file

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

View file

@ -33,6 +33,7 @@ import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,10 +30,13 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.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 {

View file

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

View file

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

View file

@ -29,6 +29,7 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.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)
}
}

View file

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

View file

@ -0,0 +1,31 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class UserAccountDataBreadcrumbs(
@Json(name = "type") override val type: String = TYPE_BREADCRUMBS,
@Json(name = "content") val content: BreadcrumbsContent
) : UserAccountData()
@JsonClass(generateAdapter = true)
internal data class BreadcrumbsContent(
@Json(name = "recent_rooms") val recentRoomIds: List<String> = emptyList()
)

View file

@ -35,5 +35,11 @@ internal abstract class AccountDataModule {
}
@Binds
abstract fun bindUpdateUserAccountDataTask(updateUserAccountDataTask: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask
abstract fun bindUpdateUserAccountDataTask(task: DefaultUpdateUserAccountDataTask): UpdateUserAccountDataTask
@Binds
abstract fun bindSaveBreadcrumbsTask(task: DefaultSaveBreadcrumbsTask): SaveBreadcrumbsTask
@Binds
abstract fun bindUpdateBreadcrumsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask
}

View file

@ -0,0 +1,67 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.user.accountdata
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.RealmList
import javax.inject.Inject
/**
* Save the Breadcrumbs roomId list in DB, either from the sync, or updated locally
*/
internal interface SaveBreadcrumbsTask : Task<SaveBreadcrumbsTask.Params, Unit> {
data class Params(
val recentRoomIds: List<String>
)
}
internal class DefaultSaveBreadcrumbsTask @Inject constructor(
private val monarchy: Monarchy
) : SaveBreadcrumbsTask {
override suspend fun execute(params: SaveBreadcrumbsTask.Params) {
monarchy.awaitTransaction { realm ->
// Get or create a breadcrumbs entity
val entity = BreadcrumbsEntity.getOrCreate(realm)
// And save the new received list
entity.recentRoomIds = RealmList<String>().apply { addAll(params.recentRoomIds) }
// Update the room summaries
// Reset all the indexes...
RoomSummaryEntity.where(realm)
.greaterThan(RoomSummaryEntityFields.BREADCRUMBS_INDEX, RoomSummaryEntity.NOT_IN_BREADCRUMBS)
.findAll()
.forEach {
it.breadcrumbsIndex = RoomSummaryEntity.NOT_IN_BREADCRUMBS
}
// ...and apply new indexes
params.recentRoomIds.forEachIndexed { index, roomId ->
RoomSummaryEntity.where(realm, roomId)
.findFirst()
?.breadcrumbsIndex = index
}
}
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.user.accountdata
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity
import im.vector.matrix.android.internal.database.query.get
import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.fetchCopied
import javax.inject.Inject
// Use the same arbitrary value than Riot-Web
private const val MAX_BREADCRUMBS_ROOMS_NUMBER = 20
internal interface UpdateBreadcrumbsTask : Task<UpdateBreadcrumbsTask.Params, Unit> {
data class Params(
val newTopRoomId: String
)
}
internal class DefaultUpdateBreadcrumbsTask @Inject constructor(
private val saveBreadcrumbsTask: SaveBreadcrumbsTask,
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val monarchy: Monarchy
) : UpdateBreadcrumbsTask {
override suspend fun execute(params: UpdateBreadcrumbsTask.Params) {
val newBreadcrumbs =
// Get the breadcrumbs entity, if any
monarchy.fetchCopied { BreadcrumbsEntity.get(it) }
?.recentRoomIds
?.apply {
// Modify the list to add the newTopRoomId first
// Ensure the newTopRoomId is not already in the list
remove(params.newTopRoomId)
// Add the newTopRoomId at first position
add(0, params.newTopRoomId)
}
?.take(MAX_BREADCRUMBS_ROOMS_NUMBER)
?: listOf(params.newTopRoomId)
// Update the DB locally, do not wait for the sync
saveBreadcrumbsTask.execute(SaveBreadcrumbsTask.Params(newBreadcrumbs))
// FIXME It can remove the previous breadcrumbs, if not synced yet
// And update account data
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.BreadcrumbsParams(
breadcrumbsContent = BreadcrumbsContent(newBreadcrumbs)
))
}
}

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.user.accountdata
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.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,

View file

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

View file

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

View file

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

View file

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

View file

@ -42,7 +42,7 @@ import im.vector.riotx.core.di.DaggerVectorComponent
import im.vector.riotx.core.di.HasVectorInjector
import im.vector.riotx.core.di.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)

View file

@ -33,10 +33,12 @@ import im.vector.riotx.features.home.LoadingFragment
import im.vector.riotx.features.home.createdirect.CreateDirectRoomDirectoryUsersFragment
import im.vector.riotx.features.home.createdirect.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
}

View file

@ -29,6 +29,7 @@ import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedVie
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
import im.vector.riotx.features.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
}

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.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)

View file

@ -0,0 +1,45 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.extensions
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController
/**
* Apply a Vertical LinearLayout Manager to the recyclerView and set the adapter from the epoxy controller
*/
fun RecyclerView.configureWith(epoxyController: EpoxyController,
itemAnimator: RecyclerView.ItemAnimator? = null,
showDivider: Boolean = false,
hasFixedSize: Boolean = true) {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
itemAnimator?.let { this.itemAnimator = it }
if (showDivider) {
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
setHasFixedSize(hasFixedSize)
adapter = epoxyController.adapter
}
/**
* To call from Fragment.onDestroyView()
*/
fun RecyclerView.cleanup() {
adapter = null
}

View file

@ -51,7 +51,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
init {
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()
}

View file

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

View file

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

View file

@ -0,0 +1,35 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.rx
import im.vector.riotx.BuildConfig
import io.reactivex.plugins.RxJavaPlugins
import timber.log.Timber
/**
* Make sure unhandled Rx error does not crash the app in production
*/
fun setupRxPlugin() {
RxJavaPlugins.setErrorHandler { throwable ->
Timber.e(throwable, "RxError")
// Avoid crash in production
if (BuildConfig.DEBUG) {
throw throwable
}
}
}

View file

@ -16,13 +16,6 @@
package im.vector.riotx.core.utils
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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,31 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.breadcrumbs
import androidx.recyclerview.widget.DefaultItemAnimator
private const val ANIM_DURATION_IN_MILLIS = 200L
class BreadcrumbsAnimator : DefaultItemAnimator() {
init {
addDuration = ANIM_DURATION_IN_MILLIS
removeDuration = ANIM_DURATION_IN_MILLIS
moveDuration = ANIM_DURATION_IN_MILLIS
changeDuration = ANIM_DURATION_IN_MILLIS
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.breadcrumbs
import android.view.View
import com.airbnb.epoxy.EpoxyController
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
class BreadcrumbsController @Inject constructor(
private val avatarRenderer: AvatarRenderer
) : EpoxyController() {
var listener: Listener? = null
private var viewState: BreadcrumbsViewState? = null
init {
// We are requesting a model build directly as the first build of epoxy is on the main thread.
// It avoids to build the whole list of breadcrumbs on the main thread.
requestModelBuild()
}
fun update(viewState: BreadcrumbsViewState) {
this.viewState = viewState
requestModelBuild()
}
override fun buildModels() {
val safeViewState = viewState ?: return
// An empty breadcrumbs list can only be temporary because when entering in a room,
// this one is added to the breadcrumbs
safeViewState.asyncBreadcrumbs.invoke()
?.forEach {
breadcrumbsItem {
id(it.roomId)
avatarRenderer(avatarRenderer)
roomId(it.roomId)
roomName(it.displayName)
avatarUrl(it.avatarUrl)
unreadNotificationCount(it.notificationCount)
showHighlighted(it.highlightCount > 0)
hasUnreadMessage(it.hasUnreadMessages)
hasDraft(it.userDrafts.isNotEmpty())
itemClickListener(
DebouncedClickListener(View.OnClickListener { _ ->
listener?.onBreadcrumbClicked(it.roomId)
})
)
}
}
}
interface Listener {
fun onBreadcrumbClicked(roomId: String)
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.breadcrumbs
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.fragmentViewModel
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.room.detail.RoomDetailSharedAction
import im.vector.riotx.features.home.room.detail.RoomDetailSharedActionViewModel
import kotlinx.android.synthetic.main.fragment_breadcrumbs.*
import javax.inject.Inject
class BreadcrumbsFragment @Inject constructor(
private val breadcrumbsController: BreadcrumbsController,
val breadcrumbsViewModelFactory: BreadcrumbsViewModel.Factory
) : VectorBaseFragment(), BreadcrumbsController.Listener {
private lateinit var sharedActionViewModel: RoomDetailSharedActionViewModel
private val breadcrumbsViewModel: BreadcrumbsViewModel by fragmentViewModel()
override fun getLayoutResId() = R.layout.fragment_breadcrumbs
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
sharedActionViewModel = activityViewModelProvider.get(RoomDetailSharedActionViewModel::class.java)
breadcrumbsViewModel.subscribe { renderState(it) }
}
override fun onDestroyView() {
breadcrumbsRecyclerView.cleanup()
super.onDestroyView()
}
private fun setupRecyclerView() {
breadcrumbsRecyclerView.configureWith(breadcrumbsController, BreadcrumbsAnimator(), hasFixedSize = false)
breadcrumbsController.listener = this
}
private fun renderState(state: BreadcrumbsViewState) {
breadcrumbsController.update(state)
}
// BreadcrumbsController.Listener **************************************************************
override fun onBreadcrumbClicked(roomId: String) {
sharedActionViewModel.post(RoomDetailSharedAction.SwitchToRoom(roomId))
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.breadcrumbs
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
@EpoxyModelClass(layout = R.layout.item_breadcrumbs)
abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var roomId: String
@EpoxyAttribute lateinit var roomName: CharSequence
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var hasUnreadMessage: Boolean = false
@EpoxyAttribute var hasDraft: Boolean = false
@EpoxyAttribute var itemClickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.setOnClickListener(itemClickListener)
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
holder.draftIndentIndicator.isVisible = hasDraft
}
class Holder : VectorEpoxyHolder() {
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.breadcrumbsUnreadCounterBadgeView)
val unreadIndentIndicator by bind<View>(R.id.breadcrumbsUnreadIndicator)
val draftIndentIndicator by bind<View>(R.id.breadcrumbsDraftBadge)
val avatarImageView by bind<ImageView>(R.id.breadcrumbsImageView)
val rootView by bind<ViewGroup>(R.id.breadcrumbsRoot)
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.breadcrumbs
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel
import io.reactivex.schedulers.Schedulers
class BreadcrumbsViewModel @AssistedInject constructor(@Assisted initialState: BreadcrumbsViewState,
private val session: Session)
: VectorViewModel<BreadcrumbsViewState, EmptyAction>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: BreadcrumbsViewState): BreadcrumbsViewModel
}
companion object : MvRxViewModelFactory<BreadcrumbsViewModel, BreadcrumbsViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: BreadcrumbsViewState): BreadcrumbsViewModel? {
val fragment: BreadcrumbsFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.breadcrumbsViewModelFactory.create(state)
}
}
init {
observeBreadcrumbs()
}
override fun handle(action: EmptyAction) {
// No op
}
// PRIVATE METHODS *****************************************************************************
private fun observeBreadcrumbs() {
session.rx()
.liveBreadcrumbs()
.observeOn(Schedulers.computation())
.execute { asyncBreadcrumbs ->
copy(asyncBreadcrumbs = asyncBreadcrumbs)
}
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.breadcrumbs
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary
data class BreadcrumbsViewState(
val asyncBreadcrumbs: Async<List<RoomSummary>> = Uninitialized
) : MvRxState

View file

@ -20,17 +20,25 @@ import android.content.Context
import android.content.Intent
import android.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"

View file

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

View file

@ -0,0 +1,26 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail
import im.vector.riotx.core.platform.VectorSharedAction
/**
* Supported navigation actions for [RoomDetailActivity]
*/
sealed class RoomDetailSharedAction : VectorSharedAction {
data class SwitchToRoom(val roomId: String) : RoomDetailSharedAction()
}

View file

@ -0,0 +1,24 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail
import im.vector.riotx.core.platform.VectorSharedActionViewModel
import javax.inject.Inject
/**
* Activity shared view model
*/
class RoomDetailSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<RoomDetailSharedAction>()

View file

@ -143,6 +143,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
timeline.addListener(this)
timeline.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()
}
}

View file

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

View file

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

View file

@ -0,0 +1,34 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.action
import androidx.recyclerview.widget.DefaultItemAnimator
private const val ANIM_DURATION_IN_MILLIS = 300L
/**
* We only want to animate the expand of the "Report content" submenu
*/
class MessageActionsAnimator : DefaultItemAnimator() {
init {
addDuration = ANIM_DURATION_IN_MILLIS
removeDuration = 0
moveDuration = 0
changeDuration = 0
}
}

View file

@ -19,7 +19,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.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)

View file

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

View file

@ -28,17 +28,16 @@ import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.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) {

View file

@ -19,14 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.format
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.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
}
}

View file

@ -25,12 +25,14 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.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,

View file

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

View file

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

View file

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

View file

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

View file

@ -123,7 +123,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
)
}
session.getRoom(roomId)?.join(emptyList(), object : MatrixCallback<Unit> {
session.getRoom(roomId)?.join(callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// 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))
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,91 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.reactions
import android.content.Context
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import im.vector.riotx.R
class EmojiDataSource(val context: Context) {
var rawData: EmojiData? = null
init {
context.resources.openRawResource(R.raw.emoji_picker_datasource).use { input ->
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(EmojiData::class.java)
val inputAsString = input.bufferedReader().use { it.readText() }
this.rawData = jsonAdapter.fromJson(inputAsString)
// this.rawData = mb.fr(InputStreamReader(it), EmojiData::class.java)
}
}
@JsonClass(generateAdapter = true)
data class EmojiData(val categories: List<EmojiCategory>,
val emojis: Map<String, EmojiItem>,
val aliases: Map<String, String>)
@JsonClass(generateAdapter = true)
data class EmojiCategory(val id: String, val name: String, val emojis: List<String>)
@JsonClass(generateAdapter = true)
data class EmojiItem(
@Json(name = "a") val name: String,
@Json(name = "b") val unicode: String,
@Json(name = "j") val keywords: List<String>?,
val k: List<String>?) {
var _emojiText: String? = null
fun emojiString() : String {
if (_emojiText == null) {
val utf8Text = unicode.split("-").joinToString("") { "\\u$it" } // "\u0048\u0065\u006C\u006C\u006F World"
_emojiText = fromUnicode(utf8Text)
}
return _emojiText!!
}
}
companion object {
fun fromUnicode(unicode: String): String {
val str = unicode.replace("\\", "")
val arr = str.split("u".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val text = StringBuffer()
for (i in 1 until arr.size) {
val hexVal = Integer.parseInt(arr[i], 16)
text.append(Character.toChars(hexVal))
}
return text.toString()
}
}
// name: 'a',
// unified: 'b',
// non_qualified: 'c',
// has_img_apple: 'd',
// has_img_google: 'e',
// has_img_twitter: 'f',
// has_img_emojione: 'g',
// has_img_facebook: 'h',
// has_img_messenger: 'i',
// keywords: 'j',
// sheet: 'k',
// emoticons: 'l',
// text: 'm',
// short_names: 'n',
// added_in: 'o',
}

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