Merge branch 'develop' into kotlinify

This commit is contained in:
Benoit Marty 2019-10-22 12:16:20 +02:00 committed by GitHub
commit cebd8136da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 1629 additions and 1117 deletions

View file

@ -8,6 +8,8 @@ Improvements:
- Persist active tab between sessions (#503)
- Do not upload file too big for the homeserver (#587)
- Handle read markers (#84)
- Mark all messages as read (#396)
- Add ability to report content (#515)
Other changes:
- Accessibility improvements to read receipts in the room timeline and reactions emoji chooser
@ -19,6 +21,7 @@ Bugfix:
- after login, the icon in the top left is a green 'A' for (all communities) rather than my avatar (#267)
- Picture uploads are unreliable, pictures are shown in wrong aspect ratio on desktop client (#517)
- Invitation notifications are not dismissed automatically if room is joined from another client (#347)
- Opening links from RiotX reuses browser tab (#599)
Translations:
-

View file

@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m
vector.debugPrivateData=false
vector.httpLogLevel=HEADERS
vector.httpLogLevel=NONE
# Note: to debug, you can put and uncomment the following lines in the file ~/.gradle/gradle.properties to override the value above
#vector.debugPrivateData=true

View file

@ -102,7 +102,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.recyclerview:recyclerview:1.1.0-beta04"
implementation "androidx.recyclerview:recyclerview:1.1.0-beta05"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
@ -110,8 +110,8 @@ dependencies {
// Network
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
implementation 'com.squareup.retrofit2:converter-moshi:2.6.2'
implementation 'com.squareup.okhttp3:okhttp:4.2.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.0'
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
implementation 'com.novoda:merlin:1.2.0'
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"

View file

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService
import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.reporting.ReportingService
import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.SendService
@ -38,6 +39,7 @@ interface Room :
ReadService,
MembershipService,
StateService,
ReportingService,
RelationService,
RoomCryptoService {

View file

@ -53,4 +53,9 @@ interface RoomService {
* @return the [LiveData] of [RoomSummary]
*/
fun liveRoomSummaries(): LiveData<List<RoomSummary>>
/**
* Mark all rooms as read
*/
fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable
}

View file

@ -50,21 +50,19 @@ interface RelationService {
/**
* Sends a reaction (emoji) to the targetedEvent.
* @param reaction the reaction (preferably emoji)
* @param targetEventId the id of the event being reacted
* @param reaction the reaction (preferably emoji)
*/
fun sendReaction(reaction: String,
targetEventId: String): Cancelable
fun sendReaction(targetEventId: String,
reaction: String): Cancelable
/**
* Undo a reaction (emoji) to the targetedEvent.
* @param reaction the reaction (preferably emoji)
* @param targetEventId the id of the event being reacted
* @param myUserId used to know if a reaction event was made by the user
* @param reaction the reaction (preferably emoji)
*/
fun undoReaction(reaction: String,
targetEventId: String,
myUserId: String) // : Cancelable
fun undoReaction(targetEventId: String,
reaction: String): Cancelable
/**
* Edit a text message body. Limited to "m.text" contentType
@ -92,7 +90,7 @@ interface RelationService {
compatibilityBodyText: String = "* $newBodyText"): Cancelable
/**
* Get's the edit history of the given event
* Get the edit history of the given event
*/
fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>)

View file

@ -0,0 +1,32 @@
/*
* 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.api.session.room.reporting
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
/**
* This interface defines methods to report content of an event.
*/
interface ReportingService {
/**
* Report content
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid
*/
fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback<Unit>): Cancelable
}

View file

@ -766,7 +766,6 @@ internal class DefaultCryptoService @Inject constructor(
*
* @param password the password
* @param anIterationCount the encryption iteration count (0 means no encryption)
* @param callback the exported keys
*/
private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray {
return withContext(coroutineDispatchers.crypto) {

View file

@ -156,7 +156,6 @@ internal class MXMegolmEncryption(
*
* @param session the session info
* @param devicesByUser the devices map
* @param callback the asynchronous callback
*/
private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo,
devicesByUser: Map<String, List<MXDeviceInfo>>) {
@ -262,7 +261,6 @@ internal class MXMegolmEncryption(
* This method must be called in getDecryptingThreadHandler() thread.
*
* @param userIds the user ids whose devices must be checked.
* @param callback the asynchronous callback
*/
private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<MXDeviceInfo> {
// We are happy to use a cached version here: we assume that if we already

View file

@ -50,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrap
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.Task
@ -81,6 +82,7 @@ import kotlin.random.Random
@SessionScope
internal class KeysBackup @Inject constructor(
@UserId private val userId: String,
private val credentials: Credentials,
private val cryptoStore: IMXCryptoStore,
private val olmDevice: MXOlmDevice,
@ -379,8 +381,6 @@ internal class KeysBackup @Inject constructor(
*/
@WorkerThread
private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
val myUserId = credentials.userId
val keysBackupVersionTrust = KeysBackupVersionTrust()
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
@ -392,7 +392,7 @@ internal class KeysBackup @Inject constructor(
return keysBackupVersionTrust
}
val mySigs = authData.signatures?.get(myUserId)
val mySigs = authData.signatures?.get(userId)
if (mySigs.isNullOrEmpty()) {
Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user")
return keysBackupVersionTrust
@ -407,7 +407,7 @@ internal class KeysBackup @Inject constructor(
}
if (deviceId != null) {
val device = cryptoStore.getUserDevice(deviceId, myUserId)
val device = cryptoStore.getUserDevice(deviceId, userId)
var isSignatureValid = false
if (device == null) {
@ -454,10 +454,8 @@ internal class KeysBackup @Inject constructor(
} else {
GlobalScope.launch(coroutineDispatchers.main) {
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
val myUserId = credentials.userId
// Get current signatures, or create an empty set
val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap()
val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap()
?: HashMap()
if (trust) {
@ -466,7 +464,7 @@ internal class KeysBackup @Inject constructor(
val deviceSignatures = objectSigner.signObject(canonicalJson)
deviceSignatures[myUserId]?.forEach { entry ->
deviceSignatures[userId]?.forEach { entry ->
myUserSignatures[entry.key] = entry.value
}
} else {
@ -482,7 +480,7 @@ internal class KeysBackup @Inject constructor(
val newMegolmBackupAuthData = authData.copy()
val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap()
newSignatures[myUserId] = myUserSignatures
newSignatures[userId] = myUserSignatures
newMegolmBackupAuthData.signatures = newSignatures
@ -1358,5 +1356,5 @@ internal class KeysBackup @Inject constructor(
* DEBUG INFO
* ========================================================================================== */
override fun toString() = "KeysBackup for ${credentials.userId}"
override fun toString() = "KeysBackup for $userId"
}

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.relation.RelationService
import im.vector.matrix.android.api.session.room.reporting.ReportingService
import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.session.room.send.DraftService
import im.vector.matrix.android.api.session.room.send.SendService
@ -44,18 +45,20 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
private val sendService: SendService,
private val draftService: DraftService,
private val stateService: StateService,
private val reportingService: ReportingService,
private val readService: ReadService,
private val cryptoService: CryptoService,
private val relationService: RelationService,
private val roomMembersService: MembershipService
) : Room,
TimelineService by timelineService,
SendService by sendService,
DraftService by draftService,
StateService by stateService,
ReadService by readService,
RelationService by relationService,
MembershipService by roomMembersService {
private val roomMembersService: MembershipService) :
Room,
TimelineService by timelineService,
SendService by sendService,
DraftService by draftService,
StateService by stateService,
ReportingService by reportingService,
ReadService by readService,
RelationService by relationService,
MembershipService by roomMembersService {
override fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>> {
val liveData = monarchy.findAllMappedWithChanges(

View file

@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
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.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.Realm
@ -41,6 +42,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
private val roomSummaryMapper: RoomSummaryMapper,
private val createRoomTask: CreateRoomTask,
private val joinRoomTask: JoinRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService {
@ -80,4 +82,12 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
}
.executeBy(taskExecutor)
}
override fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable {
return markAllRoomsReadTask
.configureWith(MarkAllRoomsReadTask.Params(roomIds)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
}

View file

@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody
import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
@ -245,4 +246,16 @@ internal interface RoomAPI {
@Path("eventId") parent_id: String,
@Body reason: Map<String, String>
): Call<SendResponse>
/**
* Reports an event as inappropriate to the server, which may then notify the appropriate people.
*
* @param roomId the room id
* @param eventId the event to report content
* @param body body containing score and reason
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}")
fun reportContent(@Path("roomId") roomId: String,
@Path("eventId") eventId: String,
@Body body: ReportContentBody): Call<Unit>
}

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.read.DefaultReadService
import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService
import im.vector.matrix.android.internal.session.room.reporting.DefaultReportingService
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
@ -40,6 +41,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
private val sendServiceFactory: DefaultSendService.Factory,
private val draftServiceFactory: DefaultDraftService.Factory,
private val stateServiceFactory: DefaultStateService.Factory,
private val reportingServiceFactory: DefaultReportingService.Factory,
private val readServiceFactory: DefaultReadService.Factory,
private val relationServiceFactory: DefaultRelationService.Factory,
private val membershipServiceFactory: DefaultMembershipService.Factory) :
@ -54,6 +56,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
sendServiceFactory.create(roomId),
draftServiceFactory.create(roomId),
stateServiceFactory.create(roomId),
reportingServiceFactory.create(roomId),
readServiceFactory.create(roomId),
cryptoService,
relationServiceFactory.create(roomId),

View file

@ -40,22 +40,16 @@ import im.vector.matrix.android.internal.session.room.membership.leaving.Default
import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask
import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask
import im.vector.matrix.android.internal.session.room.prune.PruneEventTask
import im.vector.matrix.android.internal.session.room.read.DefaultMarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
import im.vector.matrix.android.internal.session.room.relation.DefaultFetchEditHistoryTask
import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask
import im.vector.matrix.android.internal.session.room.relation.FetchEditHistoryTask
import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask
import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask
import im.vector.matrix.android.internal.session.room.relation.*
import im.vector.matrix.android.internal.session.room.reporting.DefaultReportContentTask
import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
import im.vector.matrix.android.internal.session.room.state.SendStateTask
import im.vector.matrix.android.internal.session.room.timeline.*
import im.vector.matrix.android.internal.session.room.timeline.ClearUnlinkedEventsTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import retrofit2.Retrofit
@Module
@ -110,6 +104,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindSetReadMarkersTask(setReadMarkersTask: DefaultSetReadMarkersTask): SetReadMarkersTask
@Binds
abstract fun bindMarkAllRoomsReadTask(markAllRoomsReadTask: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask
@Binds
abstract fun bindFindReactionEventForUndoTask(findReactionEventForUndoTask: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask
@ -119,6 +116,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask
@Binds
abstract fun bindReportContentTask(reportContentTask: DefaultReportContentTask): ReportContentTask
@Binds
abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask

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.matrix.android.internal.session.room.read
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface MarkAllRoomsReadTask : Task<MarkAllRoomsReadTask.Params, Unit> {
data class Params(
val roomIds: List<String>
)
}
internal class DefaultMarkAllRoomsReadTask @Inject constructor(private val readMarkersTask: SetReadMarkersTask) : MarkAllRoomsReadTask {
override suspend fun execute(params: MarkAllRoomsReadTask.Params) {
params.roomIds.forEach { roomId ->
readMarkersTask.execute(SetReadMarkersTask.Params(roomId, markAllAsRead = true))
}
}
}

View file

@ -65,7 +65,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
fun create(roomId: String): RelationService
}
override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
.also {
saveLocalEcho(it)
@ -75,13 +75,13 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
return CancelableWork(context, sendRelationWork.id)
}
override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ {
override fun undoReaction(targetEventId: String, reaction: String): Cancelable {
val params = FindReactionEventForUndoTask.Params(
roomId,
targetEventId,
reaction,
myUserId
reaction
)
// TODO We should avoid using MatrixCallback internally
val callback = object : MatrixCallback<FindReactionEventForUndoTask.Result> {
override fun onSuccess(data: FindReactionEventForUndoTask.Result) {
if (data.redactEventId == null) {
@ -89,7 +89,6 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
// TODO?
}
data.redactEventId?.let { toRedact ->
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also {
saveLocalEcho(it)
}
@ -99,7 +98,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
}
}
}
findReactionEventForUndoTask
return findReactionEventForUndoTask
.configureWith(params) {
this.retryCount = Int.MAX_VALUE
this.callback = callback

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.Task
import io.realm.Realm
import javax.inject.Inject
@ -29,8 +30,7 @@ internal interface FindReactionEventForUndoTask : Task<FindReactionEventForUndoT
data class Params(
val roomId: String,
val eventId: String,
val reaction: String,
val myUserId: String
val reaction: String
)
data class Result(
@ -38,16 +38,17 @@ internal interface FindReactionEventForUndoTask : Task<FindReactionEventForUndoT
)
}
internal class DefaultFindReactionEventForUndoTask @Inject constructor(private val monarchy: Monarchy) : FindReactionEventForUndoTask {
internal class DefaultFindReactionEventForUndoTask @Inject constructor(private val monarchy: Monarchy,
@UserId private val userId: String) : FindReactionEventForUndoTask {
override suspend fun execute(params: FindReactionEventForUndoTask.Params): FindReactionEventForUndoTask.Result {
val eventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
getReactionToRedact(realm, params.reaction, params.eventId, params.myUserId)?.eventId
getReactionToRedact(realm, params.reaction, params.eventId)?.eventId
}
return FindReactionEventForUndoTask.Result(eventId)
}
private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String, userId: String): EventEntity? {
private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String): EventEntity? {
val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() ?: return null
val rase = summary.reactionsSummary.where()

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryE
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.Task
import io.realm.Realm
import javax.inject.Inject
@ -30,8 +31,7 @@ internal interface UpdateQuickReactionTask : Task<UpdateQuickReactionTask.Params
val roomId: String,
val eventId: String,
val reaction: String,
val oppositeReaction: String,
val myUserId: String
val oppositeReaction: String
)
data class Result(
@ -40,17 +40,18 @@ internal interface UpdateQuickReactionTask : Task<UpdateQuickReactionTask.Params
)
}
internal class DefaultUpdateQuickReactionTask @Inject constructor(private val monarchy: Monarchy) : UpdateQuickReactionTask {
internal class DefaultUpdateQuickReactionTask @Inject constructor(private val monarchy: Monarchy,
@UserId private val userId: String) : UpdateQuickReactionTask {
override suspend fun execute(params: UpdateQuickReactionTask.Params): UpdateQuickReactionTask.Result {
var res: Pair<String?, List<String>?>? = null
monarchy.doWithRealm { realm ->
res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId, params.myUserId)
res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId)
}
return UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList())
}
private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String, myUserId: String): Pair<String?, List<String>?> {
private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String): Pair<String?, List<String>?> {
// the emoji reaction has been selected, we need to check if we have reacted it or not
val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
?: return Pair(reaction, null)
@ -68,7 +69,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo
val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull {
// find source event
val entity = EventEntity.where(realm, it).findFirst()
if (entity?.sender == myUserId) entity.eventId else null
if (entity?.sender == userId) entity.eventId else null
}
return Pair(reaction, toRedact)
} else {
@ -77,7 +78,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo
val toRedact = aggregationForReaction.sourceEvents.mapNotNull {
// find source event
val entity = EventEntity.where(realm, it).findFirst()
if (entity?.sender == myUserId) entity.eventId else null
if (entity?.sender == userId) entity.eventId else null
}
return Pair(null, toRedact)
}

View file

@ -0,0 +1,46 @@
/*
* 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.room.reporting
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.reporting.ReportingService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String,
private val taskExecutor: TaskExecutor,
private val reportContentTask: ReportContentTask
) : ReportingService {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): ReportingService
}
override fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback<Unit>): Cancelable {
val params = ReportContentTask.Params(roomId, eventId, score, reason)
return reportContentTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}
}

View file

@ -0,0 +1,33 @@
/*
* 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.room.reporting
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class ReportContentBody(
/**
* Required. The score to rate this content as where -100 is most offensive and 0 is inoffensive.
*/
@Json(name = "score") val score: Int,
/**
* Required. The reason the content is being reported. May be blank.
*/
@Json(name = "reason") val reason: String
)

View file

@ -0,0 +1,39 @@
/*
* 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.room.reporting
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface ReportContentTask : Task<ReportContentTask.Params, Unit> {
data class Params(
val roomId: String,
val eventId: String,
val score: Int,
val reason: String
)
}
internal class DefaultReportContentTask @Inject constructor(private val roomAPI: RoomAPI) : ReportContentTask {
override suspend fun execute(params: ReportContentTask.Params) {
return executeRequest {
apiCall = roomAPI.reportContent(params.roomId, params.eventId, ReportContentBody(params.score, params.reason))
}
}
}

View file

@ -25,7 +25,7 @@ import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject
// the receipts dictionnaries
// the receipts dictionaries
// key : $EventId
// value : dict key $UserId
// value dict key ts

View file

@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import org.junit.Assert
import org.junit.Test
@ -172,7 +173,6 @@ class PushrulesConditionTest {
}
class MockRoomService() : RoomService {
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
@ -192,9 +192,21 @@ class PushrulesConditionTest {
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return MutableLiveData()
}
override fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
}
class MockRoom(override val roomId: String, val _numberOfJoinedMembers: Int) : Room {
override fun getReadMarkerLive(): LiveData<Optional<String>> {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
override fun getMyReadReceiptLive(): LiveData<Optional<String>> {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
@ -242,7 +254,7 @@ class PushrulesConditionTest {
override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
}
override fun liveTimeLineEvent(eventId: String): LiveData<TimelineEvent> {
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
@ -250,7 +262,7 @@ class PushrulesConditionTest {
return _numberOfJoinedMembers
}
override fun liveRoomSummary(): LiveData<RoomSummary> {
override fun getRoomSummaryLive(): LiveData<Optional<RoomSummary>> {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
@ -330,11 +342,11 @@ class PushrulesConditionTest {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
override fun sendReaction(targetEventId: String, reaction: String): Cancelable {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
override fun undoReaction(reaction: String, targetEventId: String, myUserId: String) {
override fun undoReaction(targetEventId: String, reaction: String): Cancelable {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
@ -347,7 +359,7 @@ class PushrulesConditionTest {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}
override fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary> {
override fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>> {
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
}

View file

@ -281,7 +281,7 @@ dependencies {
// UI
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.android.material:material:1.1.0-alpha10'
implementation 'com.google.android.material:material:1.1.0-beta01'
implementation 'me.gujun.android:span:1.7'
implementation "ru.noties.markwon:core:$markwon_version"
implementation "ru.noties.markwon:html:$markwon_version"

View file

@ -59,7 +59,7 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.vector_home, menu)
menuInflater.inflate(R.menu.home, menu)
return true
}
}

View file

@ -42,6 +42,8 @@ import im.vector.riotx.features.home.group.GroupListFragment
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.*
import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.invite.VectorInviteView
@ -103,12 +105,10 @@ interface ScreenComponent {
fun inject(messageActionsBottomSheet: MessageActionsBottomSheet)
fun inject(viewReactionBottomSheet: ViewReactionBottomSheet)
fun inject(viewReactionsBottomSheet: ViewReactionsBottomSheet)
fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
fun inject(messageMenuFragment: MessageMenuFragment)
fun inject(vectorSettingsActivity: VectorSettingsActivity)
fun inject(createRoomFragment: CreateRoomFragment)
@ -135,8 +135,6 @@ interface ScreenComponent {
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
fun inject(quickReactionFragment: QuickReactionFragment)
fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity)
fun inject(loginActivity: LoginActivity)

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.riotx.core.dialogs
import androidx.annotation.ColorRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import im.vector.riotx.R
fun AlertDialog.withColoredButton(whichButton: Int, @ColorRes color: Int = R.color.vector_error_color): AlertDialog {
getButton(whichButton)?.setTextColor(ContextCompat.getColor(context, color))
return this
}

View file

@ -13,18 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.action
package im.vector.riotx.core.platform
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.widget.FrameLayout
import androidx.annotation.CallSuper
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.MvRxView
import com.airbnb.mvrx.MvRxViewModelStore
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.DimensionConverter
import java.util.*
/**
@ -37,10 +42,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
private lateinit var screenComponent: ScreenComponent
final override val mvrxViewId: String by lazy { mvrxPersistedViewId }
private var bottomSheetBehavior: BottomSheetBehavior<FrameLayout>? = null
val vectorBaseActivity: VectorBaseActivity by lazy {
activity as VectorBaseActivity
}
open val showExpanded = false
override fun onAttach(context: Context) {
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
super.onAttach(context)
@ -57,6 +66,17 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
super.onCreate(savedInstanceState)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
val dialog = this as? BottomSheetDialog
bottomSheetBehavior = dialog?.behavior
bottomSheetBehavior?.setPeekHeight(DimensionConverter(resources).dpToPx(400), false)
if (showExpanded) {
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mvrxViewModelStore.saveViewModels(outState)
@ -70,6 +90,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
postInvalidate()
}
@CallSuper
override fun invalidate() {
if (showExpanded) {
// Force the bottom sheet to be expanded
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
}
}
protected fun setArguments(args: Parcelable? = null) {
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
}

View file

@ -50,6 +50,7 @@ fun openUrlInExternalBrowser(context: Context, uri: Uri?) {
uri?.let {
val browserIntent = Intent(Intent.ACTION_VIEW, it).apply {
putExtra(Browser.EXTRA_APPLICATION_ID, context.packageName)
putExtra(Browser.EXTRA_CREATE_NEW_TAB, true)
}
try {

View file

@ -182,7 +182,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
}
}
return true
return super.onOptionsItemSelected(item)
}
override fun onBackPressed() {

View file

@ -30,9 +30,9 @@ sealed class RoomDetailActions {
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions()
data class SendReaction(val targetEventId: String, val reaction: String) : RoomDetailActions()
data class UndoReaction(val targetEventId: String, val reaction: String, val reason: String? = "") : RoomDetailActions()
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions()
data class SetReadMarkerAction(val eventId: String) : RoomDetailActions()
@ -49,6 +49,9 @@ sealed class RoomDetailActions {
data class ResendMessage(val eventId: String) : RoomDetailActions()
data class RemoveFailedEcho(val eventId: String) : RoomDetailActions()
data class ReportContent(val eventId: String, val reason: String, val spam: Boolean = false, val inappropriate: Boolean = false) : RoomDetailActions()
object ClearSendQueue : RoomDetailActions()
object ResendAll : RoomDetailActions()
}

View file

@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail
import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.net.Uri
@ -50,6 +51,7 @@ import com.airbnb.mvrx.*
import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.jaiselrahman.filepicker.activity.FilePickerActivity
import com.jaiselrahman.filepicker.config.Configurations
import com.jaiselrahman.filepicker.model.MediaFile
@ -68,6 +70,7 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
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
@ -93,8 +96,12 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.action.*
import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction
import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView
@ -264,6 +271,10 @@ class RoomDetailFragment :
roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState ->
syncStateView.render(syncState)
}
roomDetailViewModel.requestLiveData.observeEvent(this) {
displayRoomDetailActionResult(it)
}
}
override fun onDestroy() {
@ -420,7 +431,7 @@ class RoomDetailFragment :
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?
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
roomDetailViewModel.process(RoomDetailActions.SendReaction(eventId, reaction))
}
}
}
@ -766,17 +777,81 @@ class RoomDetailFragment :
}
private fun displayCommandError(message: String) {
AlertDialog.Builder(activity!!)
AlertDialog.Builder(requireActivity())
.setTitle(R.string.command_error)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.show()
}
private fun promptReasonToReportContent(action: SimpleAction.ReportContentCustom) {
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_report_content, null)
val input = layout.findViewById<TextInputEditText>(R.id.dialog_report_content_input)
AlertDialog.Builder(requireActivity())
.setTitle(R.string.report_content_custom_title)
.setView(layout)
.setPositiveButton(R.string.report_content_custom_submit) { _, _ ->
val reason = input.text.toString()
roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, reason))
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun displayRoomDetailActionResult(result: Async<RoomDetailActions>) {
when (result) {
is Fail -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(result.error))
.setPositiveButton(R.string.ok, null)
.show()
}
is Success -> {
when (val data = result.invoke()) {
is RoomDetailActions.ReportContent -> {
when {
data.spam -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_spam_title)
.setMessage(R.string.content_reported_as_spam_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
data.inappropriate -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_as_inappropriate_title)
.setMessage(R.string.content_reported_as_inappropriate_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
else -> {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.content_reported_title)
.setMessage(R.string.content_reported_content)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.block_user) { _, _ -> vectorBaseActivity.notImplemented("block user") }
.show()
.withColoredButton(DialogInterface.BUTTON_NEGATIVE)
}
}
}
}
}
}
}
// TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String): Boolean {
return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
val managed = permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
override fun navToRoom(roomId: String, eventId: String?): Boolean {
// Same room?
if (roomId == roomDetailArgs.roomId) {
@ -794,6 +869,14 @@ class RoomDetailFragment :
return false
}
})
if (!managed) {
// Open in external browser, in a new Tab
openUrlInExternalBrowser(requireContext(), url)
}
// In fact it is always managed
return true
}
override fun onUrlLongClicked(url: String): Boolean {
@ -901,7 +984,7 @@ class RoomDetailFragment :
override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
if (on) {
// we should test the current real state of reaction on this event
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId))
roomDetailViewModel.process(RoomDetailActions.SendReaction(informationData.eventId, reaction))
} else {
// I need to redact a reaction
roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction))
@ -909,7 +992,7 @@ class RoomDetailFragment :
}
override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) {
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
}
@ -958,23 +1041,23 @@ class RoomDetailFragment :
private fun handleActions(action: SimpleAction) {
when (action) {
is SimpleAction.AddReaction -> {
is SimpleAction.AddReaction -> {
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
}
is SimpleAction.ViewReactions -> {
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
is SimpleAction.ViewReactions -> {
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
}
is SimpleAction.Copy -> {
is SimpleAction.Copy -> {
// I need info about the current selected message :/
copyToClipboard(requireContext(), action.content, false)
val msg = requireContext().getString(R.string.copied_to_clipboard)
showSnackWithMessage(msg, Snackbar.LENGTH_SHORT)
}
is SimpleAction.Delete -> {
is SimpleAction.Delete -> {
roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason)))
}
is SimpleAction.Share -> {
is SimpleAction.Share -> {
// TODO current data communication is too limited
// Need to now the media type
// TODO bad, just POC
@ -1002,10 +1085,10 @@ class RoomDetailFragment :
}
)
}
is SimpleAction.ViewEditHistory -> {
is SimpleAction.ViewEditHistory -> {
onEditedDecorationClicked(action.messageInformationData)
}
is SimpleAction.ViewSource -> {
is SimpleAction.ViewSource -> {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
view.findViewById<TextView>(R.id.event_content_text_view)?.let {
it.text = action.content
@ -1016,7 +1099,7 @@ class RoomDetailFragment :
.setPositiveButton(R.string.ok, null)
.show()
}
is SimpleAction.ViewDecryptedSource -> {
is SimpleAction.ViewDecryptedSource -> {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null)
view.findViewById<TextView>(R.id.event_content_text_view)?.let {
it.text = action.content
@ -1027,31 +1110,40 @@ class RoomDetailFragment :
.setPositiveButton(R.string.ok, null)
.show()
}
is SimpleAction.QuickReact -> {
is SimpleAction.QuickReact -> {
// eventId,ClickedOn,Add
roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add))
}
is SimpleAction.Edit -> {
is SimpleAction.Edit -> {
roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString()))
}
is SimpleAction.Quote -> {
is SimpleAction.Quote -> {
roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString()))
}
is SimpleAction.Reply -> {
is SimpleAction.Reply -> {
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString()))
}
is SimpleAction.CopyPermalink -> {
is SimpleAction.CopyPermalink -> {
val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId)
copyToClipboard(requireContext(), permalink, false)
showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
}
is SimpleAction.Resend -> {
is SimpleAction.Resend -> {
roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId))
}
is SimpleAction.Remove -> {
is SimpleAction.Remove -> {
roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId))
}
else -> {
is SimpleAction.ReportContentSpam -> {
roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is spam", spam = true))
}
is SimpleAction.ReportContentInappropriate -> {
roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is inappropriate", inappropriate = true))
}
is SimpleAction.ReportContentCustom -> {
promptReasonToReportContent(action)
}
else -> {
Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
}
}

View file

@ -20,10 +20,7 @@ import android.net.Uri
import androidx.annotation.IdRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.airbnb.mvrx.*
import com.jakewharton.rxrelay2.BehaviorRelay
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
@ -96,6 +93,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private var timeline = room.createTimeline(eventId, timelineSettings)
// Can be used for several actions, for a one shot result
private val _requestLiveData = MutableLiveData<LiveEvent<Async<RoomDetailActions>>>()
val requestLiveData: LiveData<LiveEvent<Async<RoomDetailActions>>>
get() = _requestLiveData
// Slot to keep a pending action during permission request
var pendingAction: RoomDetailActions? = null
@ -154,6 +156,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailActions.ResendAll -> handleResendAll()
is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action)
is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead()
is RoomDetailActions.ReportContent -> handleReportContent(action)
}
}
@ -447,7 +450,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private fun handleSendReaction(action: RoomDetailActions.SendReaction) {
room.sendReaction(action.reaction, action.targetEventId)
room.sendReaction(action.targetEventId, action.reaction)
}
private fun handleRedactEvent(action: RoomDetailActions.RedactAction) {
@ -456,14 +459,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private fun handleUndoReact(action: RoomDetailActions.UndoReaction) {
room.undoReaction(action.key, action.targetEventId, session.myUserId)
room.undoReaction(action.targetEventId, action.reaction)
}
private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) {
if (action.add) {
room.sendReaction(action.selectedReaction, action.targetEventId)
room.sendReaction(action.targetEventId, action.selectedReaction)
} else {
room.undoReaction(action.selectedReaction, action.targetEventId, session.myUserId)
room.undoReaction(action.targetEventId, action.selectedReaction)
}
}
@ -710,6 +713,18 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
room.markAllAsRead(object : MatrixCallback<Any> {})
}
private fun handleReportContent(action: RoomDetailActions.ReportContent) {
room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_requestLiveData.postValue(LiveEvent(Success(action)))
}
override fun onFailure(failure: Throwable) {
_requestLiveData.postValue(LiveEvent(Fail(failure)))
}
})
}
private fun observeSyncState() {
session.rx()
.liveSyncState()

View file

@ -21,19 +21,18 @@ import android.os.Parcelable
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
import com.airbnb.epoxy.EpoxyRecyclerView
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.features.home.room.detail.timeline.action.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
import javax.inject.Inject
@Parcelize
@ -48,8 +47,8 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Inject lateinit var epoxyController: DisplayReadReceiptsController
@BindView(R.id.bottom_sheet_display_reactions_list)
lateinit var epoxyRecyclerView: EpoxyRecyclerView
@BindView(R.id.bottomSheetRecyclerView)
lateinit var recyclerView: RecyclerView
private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
@ -58,24 +57,20 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
ButterKnife.bind(this, view)
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
epoxyRecyclerView.setController(epoxyController)
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recyclerView.adapter = epoxyController.adapter
bottomSheetTitle.text = getString(R.string.read_at)
epoxyController.setData(displayReadReceiptArgs.readReceipts)
}
override fun invalidate() {
// we are not using state for this one as it's static
}
// we are not using state for this one as it's static, so no need to override invalidate()
companion object {
fun newInstance(readReceipts: List<ReadReceiptData>): DisplayReadReceiptsBottomSheet {

View file

@ -0,0 +1,69 @@
/*
* 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 android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
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
/**
* A action for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_action)
abstract class BottomSheetItemAction : VectorEpoxyModel<BottomSheetItemAction.Holder>() {
@EpoxyAttribute
@DrawableRes
var iconRes: Int = 0
@EpoxyAttribute
var textRes: Int = 0
@EpoxyAttribute
var showExpand = false
@EpoxyAttribute
var expanded = false
@EpoxyAttribute
var subMenuItem = false
@EpoxyAttribute
lateinit var listener: View.OnClickListener
override fun bind(holder: Holder) {
holder.view.setOnClickListener {
listener.onClick(it)
}
holder.startSpace.isVisible = subMenuItem
holder.icon.setImageResource(iconRes)
holder.text.setText(textRes)
holder.expand.isVisible = showExpand
if (showExpand) {
holder.expand.setImageResource(if (expanded) R.drawable.ic_material_expand_less_black else R.drawable.ic_material_expand_more_black)
}
}
class Holder : VectorEpoxyHolder() {
val startSpace by bind<View>(R.id.action_start_space)
val icon by bind<ImageView>(R.id.action_icon)
val text by bind<TextView>(R.id.action_title)
val expand by bind<ImageView>(R.id.action_expand)
}
}

View file

@ -0,0 +1,59 @@
/*
* 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 android.widget.ImageView
import android.widget.TextView
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.core.extensions.setTextOrHide
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
/**
* A message preview for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_preview)
abstract class BottomSheetItemMessagePreview : VectorEpoxyModel<BottomSheetItemMessagePreview.Holder>() {
@EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute
lateinit var informationData: MessageInformationData
@EpoxyAttribute
var senderName: String? = null
@EpoxyAttribute
lateinit var body: CharSequence
@EpoxyAttribute
var time: CharSequence? = null
override fun bind(holder: Holder) {
avatarRenderer.render(informationData.avatarUrl, informationData.senderId, senderName, holder.avatar)
holder.sender.setTextOrHide(senderName)
holder.body.text = body
holder.timestamp.setTextOrHide(time)
}
class Holder : VectorEpoxyHolder() {
val avatar by bind<ImageView>(R.id.bottom_sheet_message_preview_avatar)
val sender by bind<TextView>(R.id.bottom_sheet_message_preview_sender)
val body by bind<TextView>(R.id.bottom_sheet_message_preview_body)
val timestamp by bind<TextView>(R.id.bottom_sheet_message_preview_timestamp)
}
}

View file

@ -0,0 +1,80 @@
/*
* 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 android.graphics.Typeface
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
/**
* A quick reaction list for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_quick_reaction)
abstract class BottomSheetItemQuickReactions : VectorEpoxyModel<BottomSheetItemQuickReactions.Holder>() {
@EpoxyAttribute
lateinit var fontProvider: EmojiCompatFontProvider
@EpoxyAttribute
lateinit var texts: List<String>
@EpoxyAttribute
lateinit var selecteds: List<Boolean>
@EpoxyAttribute
var listener: Listener? = null
override fun bind(holder: Holder) {
holder.textViews.forEachIndexed { index, textView ->
textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT
textView.text = texts[index]
textView.alpha = if (selecteds[index]) 0.2f else 1f
textView.setOnClickListener {
listener?.didSelect(texts[index], !selecteds[index])
}
}
}
class Holder : VectorEpoxyHolder() {
private val quickReaction0 by bind<TextView>(R.id.quickReaction0)
private val quickReaction1 by bind<TextView>(R.id.quickReaction1)
private val quickReaction2 by bind<TextView>(R.id.quickReaction2)
private val quickReaction3 by bind<TextView>(R.id.quickReaction3)
private val quickReaction4 by bind<TextView>(R.id.quickReaction4)
private val quickReaction5 by bind<TextView>(R.id.quickReaction5)
private val quickReaction6 by bind<TextView>(R.id.quickReaction6)
private val quickReaction7 by bind<TextView>(R.id.quickReaction7)
val textViews
get() = listOf(
quickReaction0,
quickReaction1,
quickReaction2,
quickReaction3,
quickReaction4,
quickReaction5,
quickReaction6,
quickReaction7
)
}
interface Listener {
fun didSelect(emoji: String, selected: Boolean)
}
}

View file

@ -0,0 +1,52 @@
/*
* 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 android.view.View
import android.widget.TextView
import androidx.annotation.DrawableRes
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
/**
* A send state for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_message_status)
abstract class BottomSheetItemSendState : VectorEpoxyModel<BottomSheetItemSendState.Holder>() {
@EpoxyAttribute
var showProgress: Boolean = false
@EpoxyAttribute
lateinit var text: CharSequence
@EpoxyAttribute
@DrawableRes
var drawableStart: Int = 0
override fun bind(holder: Holder) {
holder.progress.isVisible = showProgress
holder.text.setCompoundDrawablesWithIntrinsicBounds(drawableStart, 0, 0, 0)
holder.text.text = text
}
class Holder : VectorEpoxyHolder() {
val progress by bind<View>(R.id.messageStatusProgress)
val text by bind<TextView>(R.id.messageStatusText)
}
}

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.riotx.features.home.room.detail.timeline.action
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_bottom_sheet_divider)
abstract class BottomSheetItemSeparator : VectorEpoxyModel<BottomSheetItemSeparator.Holder>() {
class Holder : VectorEpoxyHolder()
}

View file

@ -15,62 +15,46 @@
*/
package im.vector.riotx.features.home.room.detail.timeline.action
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.android.synthetic.main.bottom_sheet_message_actions.*
import javax.inject.Inject
/**
* Bottom sheet fragment that shows a message preview with list of contextual actions
* (Includes fragments for quick reactions and list of actions)
*/
class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment(), MessageActionsEpoxyController.MessageActionsEpoxyControllerListener {
@Inject lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory
@Inject lateinit var messageActionsEpoxyController: MessageActionsEpoxyController
@BindView(R.id.bottomSheetRecyclerView)
lateinit var recyclerView: RecyclerView
@Inject
lateinit var messageActionViewModelFactory: MessageActionsViewModel.Factory
@Inject
lateinit var avatarRenderer: AvatarRenderer
private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class)
override val showExpanded = true
private lateinit var actionHandlerModel: ActionsHandler
@BindView(R.id.bottom_sheet_message_preview_avatar)
lateinit var senderAvatarImageView: ImageView
@BindView(R.id.bottom_sheet_message_preview_sender)
lateinit var senderNameTextView: TextView
@BindView(R.id.bottom_sheet_message_preview_timestamp)
lateinit var messageTimestampText: TextView
@BindView(R.id.bottom_sheet_message_preview_body)
lateinit var messageBodyTextView: TextView
override fun injectWith(screenComponent: ScreenComponent) {
screenComponent.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_message_actions, container, false)
val view = inflater.inflate(R.layout.bottom_sheet_generic_list, container, false)
ButterKnife.bind(this, view)
return view
}
@ -78,78 +62,26 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
val cfm = childFragmentManager
var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment
if (menuActionFragment == null) {
menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
cfm.beginTransaction()
.replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment")
.commit()
}
menuActionFragment.interactionListener = object : MessageMenuFragment.InteractionListener {
override fun didSelectMenuAction(simpleAction: SimpleAction) {
actionHandlerModel.fireAction(simpleAction)
dismiss()
}
}
var quickReactionFragment = cfm.findFragmentByTag("QuickReaction") as? QuickReactionFragment
if (quickReactionFragment == null) {
quickReactionFragment = QuickReactionFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
cfm.beginTransaction()
.replace(R.id.bottom_sheet_quick_reaction_container, quickReactionFragment, "QuickReaction")
.commit()
}
quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener {
override fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String) {
actionHandlerModel.fireAction(SimpleAction.QuickReact(eventId, clickedOn, add))
dismiss()
}
}
recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
recyclerView.adapter = messageActionsEpoxyController.adapter
// Disable item animation
recyclerView.itemAnimator = null
messageActionsEpoxyController.listener = this
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
// We want to force the bottom sheet initial state to expanded
(dialog as? BottomSheetDialog)?.let { bottomSheetDialog ->
bottomSheetDialog.setOnShowListener { dialog ->
val d = dialog as BottomSheetDialog
(d.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) as? FrameLayout)?.let {
BottomSheetBehavior.from(it).state = BottomSheetBehavior.STATE_COLLAPSED
}
}
override fun didSelectMenuAction(simpleAction: SimpleAction) {
if (simpleAction is SimpleAction.ReportContent) {
// Toggle report menu
viewModel.toggleReportMenu()
} else {
actionHandlerModel.fireAction(simpleAction)
dismiss()
}
return dialog
}
override fun invalidate() = withState(viewModel) {
val body = viewModel.resolveBody(it)
if (body != null) {
bottom_sheet_message_preview.isVisible = true
senderNameTextView.text = it.senderName()
messageBodyTextView.text = body
messageTimestampText.text = it.time()
avatarRenderer.render(it.informationData.avatarUrl, it.informationData.senderId, it.senderName(), senderAvatarImageView)
} else {
bottom_sheet_message_preview.isVisible = false
}
quickReactBottomDivider.isVisible = it.canReact()
bottom_sheet_quick_reaction_container.isVisible = it.canReact()
if (it.informationData.sendState.isSending()) {
messageStatusInfo.isVisible = true
messageStatusProgress.isVisible = true
messageStatusText.text = getString(R.string.event_status_sending_message)
messageStatusText.setCompoundDrawables(null, null, null, null)
} else if (it.informationData.sendState.hasFailed()) {
messageStatusInfo.isVisible = true
messageStatusProgress.isVisible = false
messageStatusText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_warning_small, 0, 0, 0)
messageStatusText.text = getString(R.string.unable_to_send_message)
} else {
messageStatusInfo.isVisible = false
}
return@withState
messageActionsEpoxyController.setData(it)
super.invalidate()
}
companion object {

View file

@ -0,0 +1,124 @@
/*
* 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 android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Success
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
/**
* Epoxy controller for message action list
*/
class MessageActionsEpoxyController @Inject constructor(private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer,
private val fontProvider: EmojiCompatFontProvider) : TypedEpoxyController<MessageActionState>() {
var listener: MessageActionsEpoxyControllerListener? = null
override fun buildModels(state: MessageActionState) {
// Message preview
val body = state.messageBody
if (body != null) {
bottomSheetItemMessagePreview {
id("preview")
avatarRenderer(avatarRenderer)
informationData(state.informationData)
senderName(state.senderName())
body(body)
time(state.time())
}
}
// Send state
if (state.informationData.sendState.isSending()) {
bottomSheetItemSendState {
id("send_state")
showProgress(true)
text(stringProvider.getString(R.string.event_status_sending_message))
}
} else if (state.informationData.sendState.hasFailed()) {
bottomSheetItemSendState {
id("send_state")
showProgress(false)
text(stringProvider.getString(R.string.unable_to_send_message))
drawableStart(R.drawable.ic_warning_small)
}
}
// Quick reactions
if (state.canReact() && state.quickStates is Success) {
// Separator
bottomSheetItemSeparator {
id("reaction_separator")
}
bottomSheetItemQuickReactions {
id("quick_reaction")
fontProvider(fontProvider)
texts(state.quickStates()?.map { it.reaction }.orEmpty())
selecteds(state.quickStates.invoke().map { it.isSelected })
listener(object : BottomSheetItemQuickReactions.Listener {
override fun didSelect(emoji: String, selected: Boolean) {
listener?.didSelectMenuAction(SimpleAction.QuickReact(state.eventId, emoji, selected))
}
})
}
}
// Separator
bottomSheetItemSeparator {
id("actions_separator")
}
// Action
state.actions()?.forEachIndexed { index, action ->
bottomSheetItemAction {
id("action_$index")
iconRes(action.iconResId)
textRes(action.titleRes)
showExpand(action is SimpleAction.ReportContent)
expanded(state.expendedReportContentMenu)
listener(View.OnClickListener { listener?.didSelectMenuAction(action) })
}
if (action is SimpleAction.ReportContent && state.expendedReportContentMenu) {
// Special case for report content menu: add the submenu
listOf(
SimpleAction.ReportContentSpam(action.eventId),
SimpleAction.ReportContentInappropriate(action.eventId),
SimpleAction.ReportContentCustom(action.eventId)
).forEachIndexed { indexReport, actionReport ->
bottomSheetItemAction {
id("actionReport_$indexReport")
subMenuItem(true)
iconRes(actionReport.iconResId)
textRes(actionReport.titleRes)
listener(View.OnClickListener { listener?.didSelectMenuAction(actionReport) })
}
}
}
}
}
interface MessageActionsEpoxyControllerListener {
fun didSelectMenuAction(simpleAction: SimpleAction)
}
}

View file

@ -21,26 +21,48 @@ import com.squareup.inject.assisted.AssistedInject
import dagger.Lazy
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.rx.RxRoom
import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
import im.vector.riotx.core.extensions.canReact
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer
import java.text.SimpleDateFormat
import java.util.*
/**
* Quick reactions state
*/
data class ToggleState(
val reaction: String,
val isSelected: Boolean
)
data class MessageActionState(
val roomId: String,
val eventId: String,
val informationData: MessageInformationData,
val timelineEvent: Async<TimelineEvent> = Uninitialized
val timelineEvent: Async<TimelineEvent> = Uninitialized,
val messageBody: CharSequence? = null,
// For quick reactions
val quickStates: Async<List<ToggleState>> = Uninitialized,
// For actions
val actions: Async<List<SimpleAction>> = Uninitialized,
val expendedReportContentMenu: Boolean = false
) : MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
@ -49,18 +71,101 @@ data class MessageActionState(
fun senderName(): String = informationData.memberName?.toString() ?: ""
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) }
?: ""
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: ""
fun canReact() = timelineEvent()?.canReact() == true
}
fun messageBody(eventHtmlRenderer: EventHtmlRenderer?, noticeEventFormatter: NoticeEventFormatter?): CharSequence? {
/**
* Information related to an event and used to display preview in contextual bottomsheet.
*/
class MessageActionsViewModel @AssistedInject constructor(@Assisted
initialState: MessageActionState,
private val eventHtmlRenderer: Lazy<EventHtmlRenderer>,
private val session: Session,
private val noticeEventFormatter: NoticeEventFormatter,
private val stringProvider: StringProvider
) : VectorViewModel<MessageActionState>(initialState) {
private val eventId = initialState.eventId
private val informationData = initialState.informationData
private val room = session.getRoom(initialState.roomId)
@AssistedInject.Factory
interface Factory {
fun create(initialState: MessageActionState): MessageActionsViewModel
}
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.messageActionViewModelFactory.create(state)
}
}
init {
observeEvent()
observeReactions()
observeEventAction()
}
fun toggleReportMenu() = withState {
setState {
copy(
expendedReportContentMenu = it.expendedReportContentMenu.not()
)
}
}
private fun observeEvent() {
if (room == null) return
RxRoom(room)
.liveTimelineEvent(eventId)
.unwrap()
.execute {
copy(
timelineEvent = it,
messageBody = computeMessageBody(it)
)
}
}
private fun observeEventAction() {
if (room == null) return
RxRoom(room)
.liveTimelineEvent(eventId)
.map {
actionsForEvent(it)
}
.execute {
copy(actions = it)
}
}
private fun observeReactions() {
if (room == null) return
RxRoom(room)
.liveAnnotationSummary(eventId)
.map { annotations ->
quickEmojis.map { emoji ->
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
}
}
.execute {
copy(quickStates = it)
}
}
private fun computeMessageBody(timelineEvent: Async<TimelineEvent>): CharSequence? {
return when (timelineEvent()?.root?.getClearType()) {
EventType.MESSAGE -> {
val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
eventHtmlRenderer?.render(messageContent.formattedBody
?: messageContent.body)
eventHtmlRenderer.get().render(messageContent.formattedBody
?: messageContent.body)
} else {
messageContent?.body
}
@ -72,54 +177,177 @@ data class MessageActionState(
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> {
timelineEvent()?.let { noticeEventFormatter?.format(it) }
timelineEvent()?.let { noticeEventFormatter.format(it) }
}
else -> null
}
}
}
/**
* Information related to an event and used to display preview in contextual bottomsheet.
*/
class MessageActionsViewModel @AssistedInject constructor(@Assisted
initialState: MessageActionState,
private val eventHtmlRenderer: Lazy<EventHtmlRenderer>,
session: Session,
private val noticeEventFormatter: NoticeEventFormatter
) : VectorViewModel<MessageActionState>(initialState) {
private fun actionsForEvent(optionalEvent: Optional<TimelineEvent>): List<SimpleAction> {
val event = optionalEvent.getOrNull() ?: return emptyList()
private val eventId = initialState.eventId
private val room = session.getRoom(initialState.roomId)
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
?: event.root.getClearContent().toModel()
val type = messageContent?.type
@AssistedInject.Factory
interface Factory {
fun create(initialState: MessageActionState): MessageActionsViewModel
}
return arrayListOf<SimpleAction>().apply {
if (event.root.sendState.hasFailed()) {
if (canRetry(event)) {
add(SimpleAction.Resend(eventId))
}
add(SimpleAction.Remove(eventId))
} else if (event.root.sendState.isSending()) {
// TODO is uploading attachment?
if (canCancel(event)) {
add(SimpleAction.Cancel(eventId))
}
} else {
if (!event.root.isRedacted()) {
if (canReply(event, messageContent)) {
add(SimpleAction.Reply(eventId))
}
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {
if (canEdit(event, session.myUserId)) {
add(SimpleAction.Edit(eventId))
}
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.messageActionViewModelFactory.create(state)
if (canRedact(event, session.myUserId)) {
add(SimpleAction.Delete(eventId))
}
if (canCopy(type)) {
// TODO copy images? html? see ClipBoard
add(SimpleAction.Copy(messageContent!!.body))
}
if (event.canReact()) {
add(SimpleAction.AddReaction(eventId))
}
if (canQuote(event, messageContent)) {
add(SimpleAction.Quote(eventId))
}
if (canViewReactions(event)) {
add(SimpleAction.ViewReactions(informationData))
}
if (event.hasBeenEdited()) {
add(SimpleAction.ViewEditHistory(informationData))
}
if (canShare(type)) {
if (messageContent is MessageImageContent) {
session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
add(SimpleAction.Share(url))
}
}
// TODO
}
if (event.root.sendState == SendState.SENT) {
// TODO Can be redacted
// TODO sent by me or sufficient power level
}
}
add(SimpleAction.ViewSource(event.root.toContentStringWithIndent()))
if (event.isEncrypted()) {
val decryptedContent = event.root.toClearContentStringWithIndent()
?: stringProvider.getString(R.string.encryption_information_decryption_error)
add(SimpleAction.ViewDecryptedSource(decryptedContent))
}
add(SimpleAction.CopyPermalink(eventId))
if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
// not sent by me
add(SimpleAction.ReportContent(eventId))
}
}
}
}
init {
observeEvent()
private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
return false
}
private fun observeEvent() {
if (room == null) return
RxRoom(room)
.liveTimelineEvent(eventId)
.unwrap()
.execute {
copy(timelineEvent = it)
}
private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
return when (messageContent?.type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE -> true
else -> false
}
}
fun resolveBody(state: MessageActionState): CharSequence? {
return state.messageBody(eventHtmlRenderer.get(), noticeEventFormatter)
private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
return when (messageContent?.type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.FORMAT_MATRIX_HTML,
MessageType.MSGTYPE_LOCATION -> {
true
}
else -> false
}
}
private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
return event.root.senderId == myUserId
}
private fun canRetry(event: TimelineEvent): Boolean {
return event.root.sendState.hasFailed() && event.root.isTextMessage()
}
private fun canViewReactions(event: TimelineEvent): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
}
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
val messageContent = event.root.getClearContent().toModel<MessageContent>()
return event.root.senderId == myUserId && (
messageContent?.type == MessageType.MSGTYPE_TEXT
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
)
}
private fun canCopy(type: String?): Boolean {
return when (type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.FORMAT_MATRIX_HTML,
MessageType.MSGTYPE_LOCATION -> true
else -> false
}
}
private fun canShare(type: String?): Boolean {
return when (type) {
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
}
}

View file

@ -1,104 +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.home.room.detail.timeline.action
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.airbnb.mvrx.MvRx
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.platform.VectorBaseFragment
import im.vector.riotx.features.themes.ThemeUtils
import javax.inject.Inject
/**
* Fragment showing the list of available contextual action for a given message.
*/
class MessageMenuFragment : VectorBaseFragment() {
@Inject lateinit var messageMenuViewModelFactory: MessageMenuViewModel.Factory
private val viewModel: MessageMenuViewModel by fragmentViewModel(MessageMenuViewModel::class)
private var addSeparators = false
var interactionListener: InteractionListener? = null
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun getLayoutResId() = R.layout.fragment_message_menu
override fun invalidate() = withState(viewModel) { state ->
val linearLayout = view as? LinearLayout
if (linearLayout != null) {
val inflater = LayoutInflater.from(linearLayout.context)
linearLayout.removeAllViews()
var insertIndex = 0
val actions = state.actions()
actions?.forEachIndexed { index, action ->
inflateActionView(action, inflater, linearLayout)?.let {
it.setOnClickListener {
interactionListener?.didSelectMenuAction(action)
}
linearLayout.addView(it, insertIndex)
insertIndex++
if (addSeparators) {
if (index < actions.size - 1) {
linearLayout.addView(inflateSeparatorView(), insertIndex)
insertIndex++
}
}
}
}
}
}
private fun inflateActionView(action: SimpleAction, inflater: LayoutInflater, container: ViewGroup?): View? {
return inflater.inflate(R.layout.adapter_item_action, container, false)?.apply {
findViewById<ImageView>(R.id.action_icon)?.setImageResource(action.iconResId)
findViewById<TextView>(R.id.action_title)?.setText(action.titleRes)
}
}
private fun inflateSeparatorView(): View {
val frame = FrameLayout(requireContext())
frame.setBackgroundColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_list_divider_color))
frame.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, requireContext().resources.displayMetrics.density.toInt())
return frame
}
interface InteractionListener {
fun didSelectMenuAction(simpleAction: SimpleAction)
}
companion object {
fun newInstance(pa: TimelineEventFragmentArgs): MessageMenuFragment {
val args = Bundle()
args.putParcelable(MvRx.KEY_ARG, pa)
val fragment = MessageMenuFragment()
fragment.arguments = args
return fragment
}
}
}

View file

@ -1,279 +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.home.room.detail.timeline.action
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.rx.RxRoom
import im.vector.riotx.R
import im.vector.riotx.core.extensions.canReact
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) {
data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction)
data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy)
data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit)
data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote)
data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply)
data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share)
data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw)
data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash)
data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete)
data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round)
data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source)
data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source)
data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink)
data class Flag(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag)
data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0)
data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions)
data class ViewEditHistory(val messageInformationData: MessageInformationData) :
SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history)
}
data class MessageMenuState(
val roomId: String,
val eventId: String,
val informationData: MessageInformationData,
val actions: Async<List<SimpleAction>> = Uninitialized
) : MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
}
/**
* Manages list actions for a given message (copy / paste / forward...)
*/
class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: MessageMenuState,
private val session: Session,
private val stringProvider: StringProvider) : VectorViewModel<MessageMenuState>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: MessageMenuState): MessageMenuViewModel
}
private val room = session.getRoom(initialState.roomId)
?: throw IllegalStateException("Shouldn't use this ViewModel without a room")
private val eventId = initialState.eventId
private val informationData: MessageInformationData = initialState.informationData
companion object : MvRxViewModelFactory<MessageMenuViewModel, MessageMenuState> {
override fun create(viewModelContext: ViewModelContext, state: MessageMenuState): MessageMenuViewModel? {
val fragment: MessageMenuFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.messageMenuViewModelFactory.create(state)
}
}
init {
observeEvent()
}
private fun observeEvent() {
RxRoom(room)
.liveTimelineEvent(eventId)
.map {
actionsForEvent(it)
}
.execute {
copy(actions = it)
}
}
private fun actionsForEvent(optionalEvent: Optional<TimelineEvent>): List<SimpleAction> {
val event = optionalEvent.getOrNull() ?: return emptyList()
val messageContent: MessageContent? = event.annotations?.editSummary?.aggregatedContent.toModel()
?: event.root.getClearContent().toModel()
val type = messageContent?.type
return arrayListOf<SimpleAction>().apply {
if (event.root.sendState.hasFailed()) {
if (canRetry(event)) {
add(SimpleAction.Resend(eventId))
}
add(SimpleAction.Remove(eventId))
} else if (event.root.sendState.isSending()) {
// TODO is uploading attachment?
if (canCancel(event)) {
add(SimpleAction.Cancel(eventId))
}
} else {
if (!event.root.isRedacted()) {
if (canReply(event, messageContent)) {
add(SimpleAction.Reply(eventId))
}
if (canEdit(event, session.myUserId)) {
add(SimpleAction.Edit(eventId))
}
if (canRedact(event, session.myUserId)) {
add(SimpleAction.Delete(eventId))
}
if (canCopy(type)) {
// TODO copy images? html? see ClipBoard
add(SimpleAction.Copy(messageContent!!.body))
}
if (event.canReact()) {
add(SimpleAction.AddReaction(eventId))
}
if (canQuote(event, messageContent)) {
add(SimpleAction.Quote(eventId))
}
if (canViewReactions(event)) {
add(SimpleAction.ViewReactions(informationData))
}
if (event.hasBeenEdited()) {
add(SimpleAction.ViewEditHistory(informationData))
}
if (canShare(type)) {
if (messageContent is MessageImageContent) {
session.contentUrlResolver().resolveFullSize(messageContent.url)?.let { url ->
add(SimpleAction.Share(url))
}
}
// TODO
}
if (event.root.sendState == SendState.SENT) {
// TODO Can be redacted
// TODO sent by me or sufficient power level
}
}
add(SimpleAction.ViewSource(event.root.toContentStringWithIndent()))
if (event.isEncrypted()) {
val decryptedContent = event.root.toClearContentStringWithIndent()
?: stringProvider.getString(R.string.encryption_information_decryption_error)
add(SimpleAction.ViewDecryptedSource(decryptedContent))
}
add(SimpleAction.CopyPermalink(eventId))
if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
// not sent by me
add(SimpleAction.Flag(eventId))
}
}
}
}
private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
return false
}
private fun canReply(event: TimelineEvent, messageContent: MessageContent?): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
return when (messageContent?.type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE -> true
else -> false
}
}
private fun canQuote(event: TimelineEvent, messageContent: MessageContent?): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
return when (messageContent?.type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.FORMAT_MATRIX_HTML,
MessageType.MSGTYPE_LOCATION -> {
true
}
else -> false
}
}
private fun canRedact(event: TimelineEvent, myUserId: String): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
return event.root.senderId == myUserId
}
private fun canRetry(event: TimelineEvent): Boolean {
return event.root.sendState.hasFailed() && event.root.isTextMessage()
}
private fun canViewReactions(event: TimelineEvent): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
return event.annotations?.reactionsSummary?.isNotEmpty() ?: false
}
private fun canEdit(event: TimelineEvent, myUserId: String): Boolean {
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false
// TODO if user is admin or moderator
val messageContent = event.root.getClearContent().toModel<MessageContent>()
return event.root.senderId == myUserId && (
messageContent?.type == MessageType.MSGTYPE_TEXT
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
)
}
private fun canCopy(type: String?): Boolean {
return when (type) {
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE,
MessageType.FORMAT_MATRIX_HTML,
MessageType.MSGTYPE_LOCATION -> true
else -> false
}
}
private fun canShare(type: String?): Boolean {
return when (type) {
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
}
}

View file

@ -1,89 +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.home.room.detail.timeline.action
import android.graphics.Typeface
import android.os.Bundle
import android.view.View
import android.widget.TextView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.adapter_item_action_quick_reaction.*
import javax.inject.Inject
/**
* Quick Reaction Fragment (agree / like reactions)
*/
class QuickReactionFragment : VectorBaseFragment() {
private val viewModel: QuickReactionViewModel by fragmentViewModel(QuickReactionViewModel::class)
var interactionListener: InteractionListener? = null
@Inject lateinit var fontProvider: EmojiCompatFontProvider
@Inject lateinit var quickReactionViewModelFactory: QuickReactionViewModel.Factory
override fun getLayoutResId() = R.layout.adapter_item_action_quick_reaction
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
private lateinit var textViews: List<TextView>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
textViews = listOf(quickReaction0, quickReaction1, quickReaction2, quickReaction3,
quickReaction4, quickReaction5, quickReaction6, quickReaction7)
textViews.forEachIndexed { index, textView ->
textView.typeface = fontProvider.typeface ?: Typeface.DEFAULT
textView.setOnClickListener {
viewModel.didSelect(index)
}
}
}
override fun invalidate() = withState(viewModel) {
val quickReactionsStates = it.quickStates() ?: return@withState
quickReactionsStates.forEachIndexed { index, qs ->
textViews[index].text = qs.reaction
textViews[index].alpha = if (qs.isSelected) 0.2f else 1f
}
if (it.result != null) {
interactionListener?.didQuickReactWith(it.result.reaction, it.result.isSelected, it.eventId)
}
}
interface InteractionListener {
fun didQuickReactWith(clickedOn: String, add: Boolean, eventId: String)
}
companion object {
fun newInstance(pa: TimelineEventFragmentArgs): QuickReactionFragment {
val args = Bundle()
args.putParcelable(MvRx.KEY_ARG, pa)
val fragment = QuickReactionFragment()
fragment.arguments = args
return fragment
}
}
}

View file

@ -1,96 +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.home.room.detail.timeline.action
import com.airbnb.mvrx.*
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.RxRoom
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
/**
* Quick reactions state, it's a toggle with 3rd state
*/
data class ToggleState(
val reaction: String,
val isSelected: Boolean
)
data class QuickReactionState(
val roomId: String,
val eventId: String,
val informationData: MessageInformationData,
val quickStates: Async<List<ToggleState>> = Uninitialized,
val result: ToggleState? = null
/** Pair of 'clickedOn' and current toggles state*/
) : MvRxState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
}
/**
* Quick reaction view model
*/
class QuickReactionViewModel @AssistedInject constructor(@Assisted initialState: QuickReactionState,
private val session: Session) : VectorViewModel<QuickReactionState>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: QuickReactionState): QuickReactionViewModel
}
private val room = session.getRoom(initialState.roomId)
private val eventId = initialState.eventId
companion object : MvRxViewModelFactory<QuickReactionViewModel, QuickReactionState> {
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
override fun create(viewModelContext: ViewModelContext, state: QuickReactionState): QuickReactionViewModel? {
val fragment: QuickReactionFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.quickReactionViewModelFactory.create(state)
}
}
init {
observeReactions()
}
private fun observeReactions() {
if (room == null) return
RxRoom(room)
.liveAnnotationSummary(eventId)
.map { annotations ->
quickEmojis.map { emoji ->
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe
?: false)
}
}
.execute {
copy(quickStates = it)
}
}
fun didSelect(index: Int) = withState {
val selectedReaction = it.quickStates()?.get(index) ?: return@withState
val isSelected = selectedReaction.isSelected
setState {
copy(result = ToggleState(selectedReaction.reaction, !isSelected))
}
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.annotation.DrawableRes
import androidx.annotation.StringRes
import im.vector.riotx.R
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
sealed class SimpleAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) {
data class AddReaction(val eventId: String) : SimpleAction(R.string.message_add_reaction, R.drawable.ic_add_reaction)
data class Copy(val content: String) : SimpleAction(R.string.copy, R.drawable.ic_copy)
data class Edit(val eventId: String) : SimpleAction(R.string.edit, R.drawable.ic_edit)
data class Quote(val eventId: String) : SimpleAction(R.string.quote, R.drawable.ic_quote)
data class Reply(val eventId: String) : SimpleAction(R.string.reply, R.drawable.ic_reply)
data class Share(val imageUrl: String) : SimpleAction(R.string.share, R.drawable.ic_share)
data class Resend(val eventId: String) : SimpleAction(R.string.global_retry, R.drawable.ic_refresh_cw)
data class Remove(val eventId: String) : SimpleAction(R.string.remove, R.drawable.ic_trash)
data class Delete(val eventId: String) : SimpleAction(R.string.delete, R.drawable.ic_delete)
data class Cancel(val eventId: String) : SimpleAction(R.string.cancel, R.drawable.ic_close_round)
data class ViewSource(val content: String) : SimpleAction(R.string.view_source, R.drawable.ic_view_source)
data class ViewDecryptedSource(val content: String) : SimpleAction(R.string.view_decrypted_source, R.drawable.ic_view_source)
data class CopyPermalink(val eventId: String) : SimpleAction(R.string.permalink, R.drawable.ic_permalink)
data class ReportContent(val eventId: String) : SimpleAction(R.string.report_content, R.drawable.ic_flag)
data class ReportContentSpam(val eventId: String) : SimpleAction(R.string.report_content_spam, R.drawable.ic_report_spam)
data class ReportContentInappropriate(val eventId: String) : SimpleAction(R.string.report_content_inappropriate, R.drawable.ic_report_inappropriate)
data class ReportContentCustom(val eventId: String) : SimpleAction(R.string.report_content_custom, R.drawable.ic_report_custom)
data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : SimpleAction(0, 0)
data class ViewReactions(val messageInformationData: MessageInformationData) : SimpleAction(R.string.message_view_reaction, R.drawable.ic_view_reactions)
data class ViewEditHistory(val messageInformationData: MessageInformationData) :
SimpleAction(R.string.message_view_edit_history, R.drawable.ic_view_edit_history)
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.action
package im.vector.riotx.features.home.room.detail.timeline.edithistory
import android.os.Bundle
import android.view.LayoutInflater
@ -21,17 +21,20 @@ 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
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.mvrx.MvRx
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.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
import javax.inject.Inject
/**
@ -44,8 +47,8 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
@BindView(R.id.bottom_sheet_display_reactions_list)
lateinit var epoxyRecyclerView: EpoxyRecyclerView
@BindView(R.id.bottomSheetRecyclerView)
lateinit var recyclerView: RecyclerView
private val epoxyController by lazy {
ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
@ -56,22 +59,23 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
ButterKnife.bind(this, view)
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
epoxyRecyclerView.setController(epoxyController)
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
recyclerView.adapter = epoxyController.adapter
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayout.VERTICAL)
recyclerView.addItemDecoration(dividerItemDecoration)
bottomSheetTitle.text = context?.getString(R.string.message_edits)
}
override fun invalidate() = withState(viewModel) {
epoxyController.setData(it)
super.invalidate()
}
companion object {

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.action
package im.vector.riotx.features.home.room.detail.timeline.edithistory
import android.content.Context
import android.text.Spannable
@ -41,7 +41,7 @@ import timber.log.Timber
import java.util.*
/**
* Epoxy controller for reaction event list
* Epoxy controller for edit history list
*/
class ViewEditHistoryEpoxyController(private val context: Context,
val dateFormatter: VectorDateFormatter,

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.action
package im.vector.riotx.features.home.room.detail.timeline.edithistory
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import timber.log.Timber
import java.util.*

View file

@ -19,9 +19,9 @@ package im.vector.riotx.features.home.room.detail.timeline.factory
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.TextPaint
import android.text.style.AbsoluteSizeSpan
import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.view.View
import dagger.Lazy
import im.vector.matrix.android.api.permalinks.MatrixLinkify
@ -39,6 +39,8 @@ import im.vector.riotx.core.linkify.VectorLinkify
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.core.utils.containsOnlyEmojis
import im.vector.riotx.core.utils.isLocalFile
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.*
@ -51,6 +53,7 @@ import javax.inject.Inject
class MessageItemFactory @Inject constructor(
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val htmlRenderer: Lazy<EventHtmlRenderer>,
private val stringProvider: StringProvider,
@ -247,6 +250,7 @@ class MessageItemFactory @Inject constructor(
message(linkifiedBody)
}
}
.useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
.searchForPills(isFormatted)
.leftGuideline(avatarSizeProvider.leftGuideline)
.attributes(attributes)
@ -271,7 +275,13 @@ class MessageItemFactory @Inject constructor(
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.setSpan(RelativeSizeSpan(.9f), editStart, editEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
// Note: text size is set to 14sp
spannable.setSpan(
AbsoluteSizeSpan(dimensionConverter.spToPx(13)),
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
spannable.setSpan(object : ClickableSpan() {
override fun onClick(widget: View?) {
callback?.onEditedDecorationClicked(informationData)
@ -351,4 +361,8 @@ class MessageItemFactory @Inject constructor(
VectorLinkify.addLinks(spannable, true)
return spannable
}
companion object {
private const val MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT = 5
}
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.action
package im.vector.riotx.features.home.room.detail.timeline.reactions
import android.widget.TextView
import androidx.core.view.isVisible

View file

@ -14,37 +14,38 @@
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.action
package im.vector.riotx.features.home.room.detail.timeline.reactions
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
import com.airbnb.epoxy.EpoxyRecyclerView
import com.airbnb.mvrx.MvRx
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.platform.VectorBaseBottomSheetDialogFragment
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
import javax.inject.Inject
/**
* Bottom sheet displaying list of reactions for a given event ordered by timestamp
*/
class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
class ViewReactionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
private val viewModel: ViewReactionViewModel by fragmentViewModel(ViewReactionViewModel::class)
private val viewModel: ViewReactionsViewModel by fragmentViewModel(ViewReactionsViewModel::class)
@Inject lateinit var viewReactionViewModelFactory: ViewReactionViewModel.Factory
@Inject lateinit var viewReactionsViewModelFactory: ViewReactionsViewModel.Factory
@BindView(R.id.bottom_sheet_display_reactions_list)
lateinit var epoxyRecyclerView: EpoxyRecyclerView
@BindView(R.id.bottomSheetRecyclerView)
lateinit var recyclerView: RecyclerView
@Inject lateinit var epoxyController: ViewReactionsEpoxyController
@ -53,26 +54,25 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
val view = inflater.inflate(R.layout.bottom_sheet_generic_list_with_title, container, false)
ButterKnife.bind(this, view)
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
epoxyRecyclerView.setController(epoxyController)
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recyclerView.adapter = epoxyController.adapter
bottomSheetTitle.text = context?.getString(R.string.reactions)
}
override fun invalidate() = withState(viewModel) {
epoxyController.setData(it)
super.invalidate()
}
companion object {
fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionBottomSheet {
fun newInstance(roomId: String, informationData: MessageInformationData): ViewReactionsBottomSheet {
val args = Bundle()
val parcelableArgs = TimelineEventFragmentArgs(
informationData.eventId,
@ -80,7 +80,7 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
informationData
)
args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
return ViewReactionBottomSheet().apply { arguments = args }
return ViewReactionsBottomSheet().apply { arguments = args }
}
}
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.action
package im.vector.riotx.features.home.room.detail.timeline.reactions
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.action
package im.vector.riotx.features.home.room.detail.timeline.reactions
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
@ -30,6 +30,7 @@ import im.vector.matrix.rx.RxRoom
import im.vector.matrix.rx.unwrap
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import io.reactivex.Observable
import io.reactivex.Single
@ -53,10 +54,10 @@ data class ReactionInfo(
/**
* Used to display the list of members that reacted to a given event
*/
class ViewReactionViewModel @AssistedInject constructor(@Assisted
class ViewReactionsViewModel @AssistedInject constructor(@Assisted
initialState: DisplayReactionsViewState,
private val session: Session,
private val dateFormatter: VectorDateFormatter
private val session: Session,
private val dateFormatter: VectorDateFormatter
) : VectorViewModel<DisplayReactionsViewState>(initialState) {
private val roomId = initialState.roomId
@ -66,14 +67,14 @@ class ViewReactionViewModel @AssistedInject constructor(@Assisted
@AssistedInject.Factory
interface Factory {
fun create(initialState: DisplayReactionsViewState): ViewReactionViewModel
fun create(initialState: DisplayReactionsViewState): ViewReactionsViewModel
}
companion object : MvRxViewModelFactory<ViewReactionViewModel, DisplayReactionsViewState> {
companion object : MvRxViewModelFactory<ViewReactionsViewModel, DisplayReactionsViewState> {
override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionViewModel? {
val fragment: ViewReactionBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewReactionViewModelFactory.create(state)
override fun create(viewModelContext: ViewModelContext, state: DisplayReactionsViewState): ViewReactionsViewModel? {
val fragment: ViewReactionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewReactionsViewModelFactory.create(state)
}
}

View file

@ -19,14 +19,10 @@ package im.vector.riotx.features.home.room.list
import im.vector.matrix.android.api.session.room.model.RoomSummary
sealed class RoomListActions {
data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()
data class ToggleCategory(val category: RoomCategory) : RoomListActions()
data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListActions()
data class RejectInvitation(val roomSummary: RoomSummary) : RoomListActions()
data class FilterWith(val filter: String) : RoomListActions()
object MarkAllRoomsRead : RoomListActions()
}

View file

@ -18,6 +18,8 @@ package im.vector.riotx.features.home.room.list
import android.os.Bundle
import android.os.Parcelable
import android.view.Menu
import android.view.MenuItem
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
@ -78,6 +80,26 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
injector.inject(this)
}
private var hasUnreadRooms = false
override fun getMenuRes() = R.menu.room_list
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_home_mark_all_as_read -> {
roomListViewModel.accept(RoomListActions.MarkAllRoomsRead)
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onPrepareOptionsMenu(menu: Menu) {
menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = hasUnreadRooms
super.onPrepareOptionsMenu(menu)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupCreateRoomButton()
@ -180,6 +202,20 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
is Fail -> renderFailure(state.asyncFilteredRooms.error)
}
roomController.update(state)
// Mark all as read menu
when (roomListParams.displayMode) {
DisplayMode.HOME,
DisplayMode.PEOPLE,
DisplayMode.ROOMS -> {
val newValue = state.hasUnread
if (hasUnreadRooms != newValue) {
hasUnreadRooms = newValue
requireActivity().invalidateOptionsMenu()
}
}
else -> Unit
}
}
private fun renderSuccess(state: RoomListViewState) {

View file

@ -78,6 +78,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
is RoomListActions.AcceptInvitation -> handleAcceptInvitation(action)
is RoomListActions.RejectInvitation -> handleRejectInvitation(action)
is RoomListActions.FilterWith -> handleFilter(action)
is RoomListActions.MarkAllRoomsRead -> handleMarkAllRoomsRead()
}
}
@ -193,6 +194,15 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
})
}
private fun handleMarkAllRoomsRead() = withState { state ->
state.asyncFilteredRooms.invoke()
?.flatMap { it.value }
?.filter { it.membership == Membership.JOIN }
?.map { it.roomId }
?.toList()
?.let { session.markAllAsRead(it, object : MatrixCallback<Unit> {}) }
}
private fun buildRoomSummaries(rooms: List<RoomSummary>): RoomSummaries {
val invites = ArrayList<RoomSummary>()
val favourites = ArrayList<RoomSummary>()

View file

@ -20,6 +20,7 @@ import androidx.annotation.StringRes
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R
@ -67,6 +68,13 @@ data class RoomListViewState(
RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded)
}
}
val hasUnread: Boolean
get() = asyncFilteredRooms.invoke()
?.flatMap { it.value }
?.filter { it.membership == Membership.JOIN }
?.any { it.hasUnreadMessages }
?: false
}
typealias RoomSummaries = LinkedHashMap<RoomCategory, List<RoomSummary>>

View file

@ -212,8 +212,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Clear the preferences.
*
* @param context the context
*/
fun clearPreferences() {
val keysToKeep = HashSet(mKeysToKeepAfterLogout)
@ -263,7 +261,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if we have already asked the user to disable battery optimisations on android >= M devices.
*
* @param context the context
* @return true if it was already requested
*/
fun didAskUserToIgnoreBatteryOptimizations(): Boolean {
@ -272,8 +269,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Mark as requested the question to disable battery optimisations.
*
* @param context the context
*/
fun setDidAskUserToIgnoreBatteryOptimizations() {
defaultPrefs.edit {
@ -294,7 +289,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the timestamp must be displayed in 12h format
*
* @param context the context
* @return true if the time must be displayed in 12h format
*/
fun displayTimeIn12hFormat(): Boolean {
@ -304,7 +298,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the join and leave membership events should be shown in the messages list.
*
* @param context the context
* @return true if the join and leave membership events should be shown in the messages list
*/
fun showJoinLeaveMessages(): Boolean {
@ -314,7 +307,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the avatar and display name events should be shown in the messages list.
*
* @param context the context
* @return true true if the avatar and display name events should be shown in the messages list.
*/
fun showAvatarDisplayNameChangeMessages(): Boolean {
@ -324,7 +316,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells the native camera to take a photo or record a video.
*
* @param context the context
* @return true to use the native camera app to record video or take photo.
*/
fun useNativeCamera(): Boolean {
@ -334,7 +325,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the send voice feature is enabled.
*
* @param context the context
* @return true if the send voice feature is enabled.
*/
fun isSendVoiceFeatureEnabled(): Boolean {
@ -344,7 +334,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells which compression level to use by default
*
* @param context the context
* @return the selected compression level
*/
fun getSelectedDefaultMediaCompressionLevel(): Int {
@ -354,7 +343,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells which media source to use by default
*
* @param context the context
* @return the selected media source
*/
fun getSelectedDefaultMediaSource(): Int {
@ -364,7 +352,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells whether to use shutter sound.
*
* @param context the context
* @return true if shutter sound should play
*/
fun useShutterSound(): Boolean {
@ -374,7 +361,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Update the notification ringtone
*
* @param context the context
* @param uri the new notification ringtone, or null for no RingTone
*/
fun setNotificationRingTone(uri: Uri?) {
@ -399,7 +385,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Provides the selected notification ring tone
*
* @param context the context
* @return the selected ring tone or null for no RingTone
*/
fun getNotificationRingTone(): Uri? {
@ -432,7 +417,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Provide the notification ringtone filename
*
* @param context the context
* @return the filename or null if "None" is selected
*/
fun getNotificationRingToneName(): String? {
@ -455,7 +439,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Enable or disable the lazy loading
*
* @param context the context
* @param newValue true to enable lazy loading, false to disable it
*/
fun setUseLazyLoading(newValue: Boolean) {
@ -467,7 +450,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the lazy loading is enabled
*
* @param context the context
* @return true if the lazy loading of room members is enabled
*/
fun useLazyLoading(): Boolean {
@ -477,7 +459,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* User explicitly refuses the lazy loading.
*
* @param context the context
*/
fun setUserRefuseLazyLoading() {
defaultPrefs.edit {
@ -488,7 +469,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the user has explicitly refused the lazy loading
*
* @param context the context
* @return true if the user has explicitly refuse the lazy loading of room members
*/
fun hasUserRefusedLazyLoading(): Boolean {
@ -498,7 +478,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the data save mode is enabled
*
* @param context the context
* @return true if the data save mode is enabled
*/
fun useDataSaveMode(): Boolean {
@ -508,7 +487,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the conf calls must be done with Jitsi.
*
* @param context the context
* @return true if the conference call must be done with jitsi.
*/
fun useJitsiConfCall(): Boolean {
@ -518,7 +496,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the application is started on boot
*
* @param context the context
* @return true if the application must be started on boot
*/
fun autoStartOnBoot(): Boolean {
@ -528,7 +505,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the application is started on boot
*
* @param context the context
* @param value true to start the application on boot
*/
fun setAutoStartOnBoot(value: Boolean) {
@ -540,7 +516,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Provides the selected saving period.
*
* @param context the context
* @return the selected period
*/
fun getSelectedMediasSavingPeriod(): Int {
@ -550,7 +525,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Updates the selected saving period.
*
* @param context the context
* @param index the selected period index
*/
fun setSelectedMediasSavingPeriod(index: Int) {
@ -562,7 +536,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Provides the minimum last access time to keep a media file.
*
* @param context the context
* @return the min last access time (in seconds)
*/
fun getMinMediasLastAccessTime(): Long {
@ -578,7 +551,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Provides the selected saving period.
*
* @param context the context
* @return the selected period
*/
fun getSelectedMediasSavingPeriodString(): String {
@ -601,7 +573,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the markdown is enabled
*
* @param context the context
* @return true if the markdown is enabled
*/
fun isMarkdownEnabled(): Boolean {
@ -611,7 +582,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Update the markdown enable status.
*
* @param context the context
* @param isEnabled true to enable the markdown
*/
fun setMarkdownEnabled(isEnabled: Boolean) {
@ -623,7 +593,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the read receipts should be shown
*
* @param context the context
* @return true if the read receipts should be shown
*/
fun showReadReceipts(): Boolean {
@ -633,7 +602,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the message timestamps must be always shown
*
* @param context the context
* @return true if the message timestamps must be always shown
*/
fun alwaysShowTimeStamps(): Boolean {
@ -643,7 +611,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the typing notifications should be sent
*
* @param context the context
* @return true to send the typing notifs
*/
fun sendTypingNotifs(): Boolean {
@ -653,7 +620,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells of the missing notifications rooms must be displayed at left (home screen)
*
* @param context the context
* @return true to move the missed notifications to the left side
*/
fun pinMissedNotifications(): Boolean {
@ -663,7 +629,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells of the unread rooms must be displayed at left (home screen)
*
* @param context the context
* @return true to move the unread room to the left side
*/
fun pinUnreadMessages(): Boolean {
@ -673,7 +638,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the phone must vibrate when mentioning
*
* @param context the context
* @return true
*/
fun vibrateWhenMentioning(): Boolean {
@ -683,7 +647,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if a dialog has been displayed to ask to use the analytics tracking (piwik, matomo, etc.).
*
* @param context the context
* @return true if a dialog has been displayed to ask to use the analytics tracking
*/
fun didAskToUseAnalytics(): Boolean {
@ -693,7 +656,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* To call if the user has been asked for analytics tracking.
*
* @param context the context
*/
fun setDidAskToUseAnalytics() {
defaultPrefs.edit {
@ -704,7 +666,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the analytics tracking is authorized (piwik, matomo, etc.).
*
* @param context the context
* @return true if the analytics tracking is authorized
*/
fun useAnalytics(): Boolean {
@ -714,7 +675,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Enable or disable the analytics tracking.
*
* @param context the context
* @param useAnalytics true to enable the analytics tracking
*/
fun setUseAnalytics(useAnalytics: Boolean) {
@ -726,7 +686,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if media should be previewed before sending
*
* @param context the context
* @return true to preview media
*/
fun previewMediaWhenSending(): Boolean {
@ -736,7 +695,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if message should be send by pressing enter on the soft keyboard
*
* @param context the context
* @return true to send message with enter
*/
fun sendMessageWithEnter(): Boolean {
@ -746,7 +704,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if the rage shake is used.
*
* @param context the context
* @return true if the rage shake is used
*/
fun useRageshake(): Boolean {
@ -756,7 +713,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Update the rage shake status.
*
* @param context the context
* @param isEnabled true to enable the rage shake
*/
fun setUseRageshake(isEnabled: Boolean) {
@ -768,7 +724,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
/**
* Tells if all the events must be displayed ie even the redacted events.
*
* @param context the context
* @return true to display all the events even the redacted ones.
*/
fun displayAllEvents(): Boolean {

View file

@ -0,0 +1,8 @@
<vector android:autoMirrored="true" android:height="22dp"
android:viewportHeight="22" android:viewportWidth="22"
android:width="22dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M21,1L10,12M21,1l-7,20 -4,-9 -9,-4z"
android:strokeColor="#9E9E9E" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector android:autoMirrored="true" android:height="22dp"
android:viewportHeight="22" android:viewportWidth="22"
android:width="22dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M11,11m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeColor="#9E9E9E" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M3.93,3.93l14.14,14.14"
android:strokeColor="#9E9E9E" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View file

@ -0,0 +1,8 @@
<vector android:autoMirrored="true" android:height="22dp"
android:viewportHeight="22" android:viewportWidth="18"
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000" android:fillType="evenOdd"
android:pathData="M1,14s1,-1 4,-1 5,2 8,2 4,-1 4,-1V2s-1,1 -4,1 -5,-2 -8,-2 -4,1 -4,1v12zM1,21v-7"
android:strokeColor="#9E9E9E" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bottomSheetRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fadeScrollbars="false"
android:scrollbars="vertical"
tools:itemCount="5"
tools:listitem="@layout/item_bottom_sheet_action" />

View file

@ -3,29 +3,25 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="400dp"
android:orientation="vertical">
<TextView
android:id="@+id/bottomSheetTitle"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:padding="8dp"
android:textColor="?android:textColorSecondary"
android:textSize="16sp"
tools:text="@string/reactions" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/bottom_sheet_display_reactions_list"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/bottomSheetRecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:fadeScrollbars="false"
android:orientation="vertical"
android:scrollbars="vertical"
tools:itemCount="15"
tools:listitem="@layout/item_simple_reaction_info">
tools:listitem="@layout/item_simple_reaction_info" />
</com.airbnb.epoxy.EpoxyRecyclerView>
</LinearLayout>

View file

@ -1,149 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/bottom_sheet_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottom_sheet_message_preview"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/bottom_sheet_message_preview_avatar"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="@dimen/layout_horizontal_margin"
android:adjustViewBounds="true"
android:background="@drawable/circle"
android:contentDescription="@string/avatar"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/bottom_sheet_message_preview_sender"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:ellipsize="end"
android:fontFamily="sans-serif-bold"
android:singleLine="true"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/bottom_sheet_message_preview_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:maxLines="3"
android:textColor="?riotx_text_secondary"
android:textIsSelectable="false"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_timestamp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " />
<TextView
android:id="@+id/bottom_sheet_message_preview_timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:layout_marginBottom="8dp"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_body"
tools:text="Friday 8pm" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/messageStatusInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="4dp"
android:layout_marginEnd="16dp">
<ProgressBar
android:id="@+id/messageStatusProgress"
style="?android:attr/progressBarStyleSmall"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center"
android:layout_marginEnd="4dp"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/messageStatusText"
android:textColor="?riotx_text_secondary"
android:textStyle="bold"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:drawableStart="@drawable/ic_warning_small"
android:drawablePadding="4dp"
tools:text="@string/unable_to_send_message" />
</LinearLayout>
<View
android:id="@+id/quickReactTopDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color" />
<FrameLayout
android:id="@+id/bottom_sheet_quick_reaction_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:background="@android:color/holo_green_light"
tools:layout_height="180dp" />
<View
android:id="@+id/quickReactBottomDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color" />
<FrameLayout
android:id="@+id/bottom_sheet_menu_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:background="@android:color/holo_blue_dark"
tools:layout_height="250dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingLeft="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingRight="?dialogPreferredPadding">
<com.google.android.material.textfield.TextInputLayout
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/report_content_custom_hint"
app:counterEnabled="true"
app:counterMaxLength="240">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_report_content_input"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"/>

View file

@ -2,23 +2,29 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="50dp"
android:orientation="horizontal"
android:paddingLeft="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingRight="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp"
tools:layout_height="50dp">
android:paddingBottom="8dp">
<Space
android:id="@+id/action_start_space"
android:layout_width="32dp"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible" />
<ImageView
android:id="@+id/action_icon"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:tint="?riotx_text_secondary"
@ -28,11 +34,19 @@
android:id="@+id/action_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:textColor="?riotx_text_secondary"
android:textSize="17sp"
tools:text="@string/delete" />
<ImageView
android:id="@+id/action_expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="?riotx_text_secondary"
android:visibility="gone"
tools:src="@drawable/ic_material_expand_more_black"
tools:visibility="visible" />
</LinearLayout>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/quickReactTopDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color" />

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bottom_sheet_message_preview"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/bottom_sheet_message_preview_avatar"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="@dimen/layout_horizontal_margin"
android:adjustViewBounds="true"
android:background="@drawable/circle"
android:contentDescription="@string/avatar"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
tools:src="@tools:sample/avatars" />
<TextView
android:id="@+id/bottom_sheet_message_preview_sender"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:ellipsize="end"
android:fontFamily="sans-serif-bold"
android:singleLine="true"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/bottom_sheet_message_preview_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:maxLines="3"
android:textColor="?riotx_text_secondary"
android:textIsSelectable="false"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_timestamp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_sender"
tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " />
<TextView
android:id="@+id/bottom_sheet_message_preview_timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginRight="@dimen/layout_horizontal_margin"
android:layout_marginBottom="8dp"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_body"
tools:text="Friday 8pm" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/messageStatusInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="4dp">
<ProgressBar
android:id="@+id/messageStatusProgress"
style="?android:attr/progressBarStyleSmall"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="center"
android:layout_marginEnd="4dp"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/messageStatusText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:drawableStart="@drawable/ic_warning_small"
android:drawablePadding="4dp"
android:textColor="?riotx_text_secondary"
android:textStyle="bold"
tools:text="@string/unable_to_send_message" />
</LinearLayout>

View file

@ -117,7 +117,16 @@
android:layout_marginBottom="4dp"
app:dividerDrawable="@drawable/reaction_divider"
app:flexWrap="wrap"
app:showDivider="middle" />
app:showDivider="middle"
tools:background="#F0E0F0"
tools:layout_height="40dp">
<!-- ReactionButtons will be added here in the code -->
<!--im.vector.riotx.features.reactions.widget.ReactionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content" /-->
</com.google.android.flexbox.FlexboxLayout>
<im.vector.riotx.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView"

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_home_mark_all_as_read"
android:icon="@drawable/ic_material_done"
android:title="@string/action_mark_all_as_read" />
</menu>

View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.matrix.vector.activity.VectorHomeActivity">
<item
android:id="@+id/ic_action_global_search"
android:icon="@drawable/ic_material_search_white"
android:title="@string/action_global_search"
app:showAsAction="collapseActionView" />
<item
android:id="@+id/ic_action_historical"
android:icon="@drawable/ic_material_history_white"
android:title="@string/action_historical"
app:showAsAction="collapseActionView" />
<item
android:id="@+id/ic_action_mark_all_as_read"
android:icon="@drawable/ic_material_done_all_white"
android:title="@string/action_mark_all_as_read"
app:showAsAction="collapseActionView" />
</menu>

View file

@ -37,4 +37,19 @@
<string name="error_file_too_big">"The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."</string>
<string name="report_content_spam">"It's spam"</string>
<string name="report_content_inappropriate">"It's inappropriate"</string>
<string name="report_content_custom">"Custom report"</string>
<string name="report_content_custom_title">"Report this content"</string>
<string name="report_content_custom_hint">"Reason for reporting this content"</string>
<string name="report_content_custom_submit">"REPORT"</string>
<string name="block_user">"BLOCK USER"</string>
<string name="content_reported_title">"Content reported"</string>
<string name="content_reported_content">"This content was reported.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
<string name="content_reported_as_spam_title">"Reported as spam"</string>
<string name="content_reported_as_spam_content">"This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
<string name="content_reported_as_inappropriate_title">"Reported as inappropriate"</string>
<string name="content_reported_as_inappropriate_content">"This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
</resources>