mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-26 03:16:02 +03:00
Merge branch 'develop' into kotlinify
This commit is contained in:
commit
cebd8136da
78 changed files with 1629 additions and 1117 deletions
|
@ -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:
|
||||
-
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>>)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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) } }
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -182,7 +182,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
|||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
|
@ -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,
|
|
@ -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.*
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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>>
|
||||
|
|
|
@ -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 {
|
||||
|
|
8
vector/src/main/res/drawable/ic_report_custom.xml
Normal file
8
vector/src/main/res/drawable/ic_report_custom.xml
Normal 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>
|
12
vector/src/main/res/drawable/ic_report_inappropriate.xml
Normal file
12
vector/src/main/res/drawable/ic_report_inappropriate.xml
Normal 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>
|
8
vector/src/main/res/drawable/ic_report_spam.xml
Normal file
8
vector/src/main/res/drawable/ic_report_spam.xml
Normal 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>
|
11
vector/src/main/res/layout/bottom_sheet_generic_list.xml
Normal file
11
vector/src/main/res/layout/bottom_sheet_generic_list.xml
Normal 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" />
|
|
@ -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>
|
|
@ -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>
|
28
vector/src/main/res/layout/dialog_report_content.xml
Normal file
28
vector/src/main/res/layout/dialog_report_content.xml
Normal 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>
|
|
@ -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"/>
|
|
@ -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>
|
6
vector/src/main/res/layout/item_bottom_sheet_divider.xml
Normal file
6
vector/src/main/res/layout/item_bottom_sheet_divider.xml
Normal 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" />
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
9
vector/src/main/res/menu/room_list.xml
Normal file
9
vector/src/main/res/menu/room_list.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue