Merge branch 'develop' into feature/attachments
This commit is contained in:
commit
2974f8b200
@ -10,6 +10,8 @@ Improvements:
|
||||
- Handle read markers (#84)
|
||||
- Attachments: start using system pickers (#52)
|
||||
- Attachments: start handling incoming share (#58)
|
||||
- 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
|
||||
@ -21,6 +23,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:
|
||||
-
|
||||
|
@ -40,28 +40,45 @@ Please add a line to the top of the file `CHANGES.md` describing your change.
|
||||
|
||||
Make sure the following commands execute without any error:
|
||||
|
||||
> ./tools/check/check_code_quality.sh
|
||||
#### Internal tool
|
||||
|
||||
> curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.34.2/ktlint && chmod a+x ktlint
|
||||
> ./ktlint --android -v
|
||||
<pre>
|
||||
./tools/check/check_code_quality.sh
|
||||
</pre>
|
||||
|
||||
#### ktlint
|
||||
|
||||
<pre>
|
||||
curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.34.2/ktlint && chmod a+x ktlint
|
||||
./ktlint --android --experimental -v
|
||||
</pre>
|
||||
|
||||
Note that you can run
|
||||
|
||||
> ./ktlint --android -v -F
|
||||
<pre>
|
||||
./ktlint --android --experimental -v -F
|
||||
</pre>
|
||||
|
||||
For ktlint to fix some detected errors for you
|
||||
For ktlint to fix some detected errors for you (you still have to check and commit the fix of course)
|
||||
|
||||
> ./gradlew lintGplayRelease
|
||||
#### lint
|
||||
|
||||
<pre>
|
||||
./gradlew lintGplayRelease
|
||||
./gradlew lintFdroidRelease
|
||||
</pre>
|
||||
|
||||
### Unit tests
|
||||
|
||||
Make sure the following commands execute without any error:
|
||||
|
||||
> ./gradlew testGplayReleaseUnitTest
|
||||
<pre>
|
||||
./gradlew testGplayReleaseUnitTest
|
||||
</pre>
|
||||
|
||||
### Tests
|
||||
|
||||
RiotX is currently supported on Android Jelly Bean (API 16+): please test your change on an Android device (or Android emulator) running with API 16. Many issues can happen (including crashes) on older devices.
|
||||
RiotX is currently supported on Android KitKat (API 19+): please test your change on an Android device (or Android emulator) running with API 19. Many issues can happen (including crashes) on older devices.
|
||||
Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient.
|
||||
|
||||
### Internationalisation
|
||||
|
@ -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"
|
||||
|
@ -19,7 +19,6 @@ package im.vector.matrix.android.api.extensions
|
||||
import im.vector.matrix.android.api.comparators.DatedObjectComparators
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import java.util.Collections
|
||||
|
||||
/* ==========================================================================================
|
||||
* MXDeviceInfo
|
||||
@ -29,6 +28,6 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
|
||||
?.chunked(4)
|
||||
?.joinToString(separator = " ")
|
||||
|
||||
fun List<DeviceInfo>.sortByLastSeen() {
|
||||
Collections.sort(this, DatedObjectComparators.descComparator)
|
||||
fun MutableList<DeviceInfo>.sortByLastSeen() {
|
||||
sortWith(DatedObjectComparators.descComparator)
|
||||
}
|
||||
|
@ -30,9 +30,9 @@ object MatrixLinkify {
|
||||
*
|
||||
* @param spannable the text in which the matrix items has to be clickable.
|
||||
*/
|
||||
fun addLinks(spannable: Spannable?, callback: MatrixPermalinkSpan.Callback?): Boolean {
|
||||
fun addLinks(spannable: Spannable, callback: MatrixPermalinkSpan.Callback?): Boolean {
|
||||
// sanity checks
|
||||
if (spannable.isNullOrEmpty()) {
|
||||
if (spannable.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
val text = spannable.toString()
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.api.permalinks
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
|
||||
/**
|
||||
@ -48,7 +47,7 @@ object PermalinkFactory {
|
||||
* @return the permalink, or null in case of error
|
||||
*/
|
||||
fun createPermalink(id: String): String? {
|
||||
return if (TextUtils.isEmpty(id)) {
|
||||
return if (id.isEmpty()) {
|
||||
null
|
||||
} else MATRIX_TO_URL_BASE + escape(id)
|
||||
}
|
||||
@ -71,11 +70,11 @@ object PermalinkFactory {
|
||||
* @param url the universal link, Ex: "https://matrix.to/#/@benoit:matrix.org"
|
||||
* @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink
|
||||
*/
|
||||
fun getLinkedId(url: String?): String? {
|
||||
val isSupported = url != null && url.startsWith(MATRIX_TO_URL_BASE)
|
||||
fun getLinkedId(url: String): String? {
|
||||
val isSupported = url.startsWith(MATRIX_TO_URL_BASE)
|
||||
|
||||
return if (isSupported) {
|
||||
url!!.substring(MATRIX_TO_URL_BASE.length)
|
||||
url.substring(MATRIX_TO_URL_BASE.length)
|
||||
} else null
|
||||
}
|
||||
|
||||
@ -86,6 +85,6 @@ object PermalinkFactory {
|
||||
* @return the escaped id
|
||||
*/
|
||||
private fun escape(id: String): String {
|
||||
return id.replace("/".toRegex(), "%2F")
|
||||
return id.replace("/", "%2F")
|
||||
}
|
||||
}
|
||||
|
@ -15,13 +15,11 @@
|
||||
*/
|
||||
package im.vector.matrix.android.api.pushrules
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import timber.log.Timber
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
|
||||
|
||||
@ -34,7 +32,7 @@ class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
|
||||
}
|
||||
|
||||
fun isSatisfied(event: Event, displayName: String): Boolean {
|
||||
var message = when (event.type) {
|
||||
val message = when (event.type) {
|
||||
EventType.MESSAGE -> {
|
||||
event.content.toModel<MessageContent>()
|
||||
}
|
||||
@ -59,20 +57,18 @@ class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
|
||||
*/
|
||||
fun caseInsensitiveFind(subString: String, longString: String): Boolean {
|
||||
// add sanity checks
|
||||
if (TextUtils.isEmpty(subString) || TextUtils.isEmpty(longString)) {
|
||||
if (subString.isEmpty() || longString.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
var res = false
|
||||
|
||||
try {
|
||||
val pattern = Pattern.compile("(\\W|^)" + Pattern.quote(subString) + "(\\W|$)", Pattern.CASE_INSENSITIVE)
|
||||
res = pattern.matcher(longString).find()
|
||||
val regex = Regex("(\\W|^)" + Regex.escape(subString) + "(\\W|$)", RegexOption.IGNORE_CASE)
|
||||
return regex.containsMatchIn(longString)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## caseInsensitiveFind() : failed")
|
||||
}
|
||||
|
||||
return res
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.events.model
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
@ -35,18 +34,16 @@ typealias Content = JsonDict
|
||||
* This methods is a facility method to map a json content to a model.
|
||||
*/
|
||||
inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
|
||||
return this?.let {
|
||||
val moshi = MoshiProvider.providesMoshi()
|
||||
val moshiAdapter = moshi.adapter(T::class.java)
|
||||
return try {
|
||||
moshiAdapter.fromJsonValue(it)
|
||||
} catch (e: Exception) {
|
||||
if (catchError) {
|
||||
Timber.e(e, "To model failed : $e")
|
||||
null
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
val moshi = MoshiProvider.providesMoshi()
|
||||
val moshiAdapter = moshi.adapter(T::class.java)
|
||||
return try {
|
||||
moshiAdapter.fromJsonValue(this)
|
||||
} catch (e: Exception) {
|
||||
if (catchError) {
|
||||
Timber.e(e, "To model failed : $e")
|
||||
null
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -55,12 +52,10 @@ inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
|
||||
* This methods is a facility method to map a model to a json Content
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T> T?.toContent(): Content? {
|
||||
return this?.let {
|
||||
val moshi = MoshiProvider.providesMoshi()
|
||||
val moshiAdapter = moshi.adapter(T::class.java)
|
||||
return moshiAdapter.toJsonValue(it) as Content
|
||||
}
|
||||
inline fun <reified T> T.toContent(): Content {
|
||||
val moshi = MoshiProvider.providesMoshi()
|
||||
val moshiAdapter = moshi.adapter(T::class.java)
|
||||
return moshiAdapter.toJsonValue(this) as Content
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,7 +101,7 @@ data class Event(
|
||||
* @return true if this event is encrypted.
|
||||
*/
|
||||
fun isEncrypted(): Boolean {
|
||||
return TextUtils.equals(type, EventType.ENCRYPTED)
|
||||
return type == EventType.ENCRYPTED
|
||||
}
|
||||
|
||||
/**
|
||||
@ -139,7 +134,7 @@ data class Event(
|
||||
}
|
||||
|
||||
fun toContentStringWithIndent(): String {
|
||||
val contentMap = toContent()?.toMutableMap() ?: HashMap()
|
||||
val contentMap = toContent().toMutableMap()
|
||||
return JSONObject(contentMap).toString(4)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -16,11 +16,9 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content.
|
||||
@ -45,14 +43,8 @@ data class PowerLevels(
|
||||
* @param userId the user id
|
||||
* @return the power level
|
||||
*/
|
||||
fun getUserPowerLevel(userId: String): Int {
|
||||
// sanity check
|
||||
if (!TextUtils.isEmpty(userId)) {
|
||||
val powerLevel = users[userId]
|
||||
return powerLevel ?: usersDefault
|
||||
}
|
||||
|
||||
return usersDefault
|
||||
fun getUserPowerLevel(userId: String): Int {
|
||||
return users.getOrElse(userId) { usersDefault }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,10 +53,8 @@ data class PowerLevels(
|
||||
* @param userId the user
|
||||
* @param powerLevel the new power level
|
||||
*/
|
||||
fun setUserPowerLevel(userId: String?, powerLevel: Int) {
|
||||
if (null != userId) {
|
||||
users[userId] = Integer.valueOf(powerLevel)
|
||||
}
|
||||
fun setUserPowerLevel(userId: String, powerLevel: Int) {
|
||||
users[userId] = powerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
@ -74,8 +64,8 @@ data class PowerLevels(
|
||||
* @param userId the user id
|
||||
* @return true if the user can send the event
|
||||
*/
|
||||
fun maySendEventOfType(eventTypeString: String, userId: String): Boolean {
|
||||
return if (!TextUtils.isEmpty(eventTypeString) && !TextUtils.isEmpty(userId)) {
|
||||
fun maySendEventOfType(eventTypeString: String, userId: String): Boolean {
|
||||
return if (eventTypeString.isNotEmpty() && userId.isNotEmpty()) {
|
||||
getUserPowerLevel(userId) >= minimumPowerLevelForSendingEventAsMessage(eventTypeString)
|
||||
} else false
|
||||
}
|
||||
@ -86,8 +76,8 @@ data class PowerLevels(
|
||||
* @param userId the user id
|
||||
* @return true if the user can send a room message
|
||||
*/
|
||||
fun maySendMessage(userId: String): Boolean {
|
||||
return maySendEventOfType(EventType.MESSAGE, userId)
|
||||
fun maySendMessage(userId: String): Boolean {
|
||||
return maySendEventOfType(EventType.MESSAGE, userId)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,7 +87,7 @@ data class PowerLevels(
|
||||
* @param eventTypeString the type of event (in Event.EVENT_TYPE_XXX values)
|
||||
* @return the required minimum power level.
|
||||
*/
|
||||
fun minimumPowerLevelForSendingEventAsMessage(eventTypeString: String?): Int {
|
||||
fun minimumPowerLevelForSendingEventAsMessage(eventTypeString: String?): Int {
|
||||
return events[eventTypeString] ?: eventsDefault
|
||||
}
|
||||
|
||||
@ -108,7 +98,7 @@ data class PowerLevels(
|
||||
* @param eventTypeString the type of event (in Event.EVENT_TYPE_STATE_ values).
|
||||
* @return the required minimum power level.
|
||||
*/
|
||||
fun minimumPowerLevelForSendingEventAsStateEvent(eventTypeString: String?): Int {
|
||||
fun minimumPowerLevelForSendingEventAsStateEvent(eventTypeString: String?): Int {
|
||||
return events[eventTypeString] ?: stateDefault
|
||||
}
|
||||
|
||||
@ -118,18 +108,14 @@ data class PowerLevels(
|
||||
* @param key the notification key
|
||||
* @return the level
|
||||
*/
|
||||
fun notificationLevel(key: String?): Int {
|
||||
if (null != key && notifications.containsKey(key)) {
|
||||
val valAsVoid = notifications[key]
|
||||
fun notificationLevel(key: String): Int {
|
||||
val valAsVoid = notifications[key] ?: return 50
|
||||
|
||||
// the first implementation was a string value
|
||||
return if (valAsVoid is String) {
|
||||
Integer.parseInt(valAsVoid)
|
||||
} else {
|
||||
valAsVoid as Int
|
||||
}
|
||||
// the first implementation was a string value
|
||||
return if (valAsVoid is String) {
|
||||
valAsVoid.toInt()
|
||||
} else {
|
||||
valAsVoid as Int
|
||||
}
|
||||
|
||||
return 50
|
||||
}
|
||||
}
|
||||
|
@ -145,15 +145,7 @@ class CreateRoomParams {
|
||||
*/
|
||||
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) {
|
||||
// Remove the existing value if any.
|
||||
if (initialStates != null && !initialStates!!.isEmpty()) {
|
||||
val newInitialStates = ArrayList<Event>()
|
||||
for (event in initialStates!!) {
|
||||
if (event.getClearType() != EventType.STATE_HISTORY_VISIBILITY) {
|
||||
newInitialStates.add(event)
|
||||
}
|
||||
}
|
||||
initialStates = newInitialStates
|
||||
}
|
||||
initialStates?.removeAll { it.getClearType() == EventType.STATE_HISTORY_VISIBILITY }
|
||||
|
||||
if (historyVisibility != null) {
|
||||
val contentMap = HashMap<String, RoomHistoryVisibility>()
|
||||
|
@ -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
|
||||
}
|
@ -21,7 +21,6 @@ package im.vector.matrix.android.internal.crypto
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import arrow.core.Try
|
||||
import com.squareup.moshi.Types
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import dagger.Lazy
|
||||
@ -359,29 +358,16 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
*/
|
||||
override fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?) {
|
||||
// build a devices map
|
||||
val devicesIdListByUserId = HashMap<String, List<String>>()
|
||||
val devicesIdListByUserId = devices.groupBy({ it.userId }, { it.deviceId })
|
||||
|
||||
for (di in devices) {
|
||||
var deviceIdsList: MutableList<String>? = devicesIdListByUserId[di.userId]?.toMutableList()
|
||||
|
||||
if (null == deviceIdsList) {
|
||||
deviceIdsList = ArrayList()
|
||||
devicesIdListByUserId[di.userId] = deviceIdsList
|
||||
}
|
||||
deviceIdsList.add(di.deviceId)
|
||||
}
|
||||
|
||||
val userIds = devicesIdListByUserId.keys
|
||||
|
||||
for (userId in userIds) {
|
||||
for ((userId, deviceIds) in devicesIdListByUserId) {
|
||||
val storedDeviceIDs = cryptoStore.getUserDevices(userId)
|
||||
|
||||
// sanity checks
|
||||
if (null != storedDeviceIDs) {
|
||||
var isUpdated = false
|
||||
val deviceIds = devicesIdListByUserId[userId]
|
||||
|
||||
deviceIds?.forEach { deviceId ->
|
||||
deviceIds.forEach { deviceId ->
|
||||
val device = storedDeviceIDs[deviceId]
|
||||
|
||||
// assume if the device is either verified or blocked
|
||||
@ -549,16 +535,10 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
val t0 = System.currentTimeMillis()
|
||||
Timber.v("## encryptEventContent() starts")
|
||||
runCatching {
|
||||
safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
|
||||
}
|
||||
.fold(
|
||||
{
|
||||
Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
|
||||
callback.onSuccess(MXEncryptEventContentResult(it, EventType.ENCRYPTED))
|
||||
},
|
||||
{ callback.onFailure(it) }
|
||||
|
||||
)
|
||||
val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
|
||||
Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
|
||||
MXEncryptEventContentResult(content, EventType.ENCRYPTED)
|
||||
}.foldToCallback(callback)
|
||||
} else {
|
||||
val algorithm = getEncryptionAlgorithm(roomId)
|
||||
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
|
||||
@ -776,7 +756,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
runCatching {
|
||||
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
|
||||
}.fold(callback::onSuccess, callback::onFailure)
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
@ -785,7 +765,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) {
|
||||
@ -813,8 +792,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
Try {
|
||||
runCatching {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
Timber.v("## importRoomKeys starts")
|
||||
|
||||
val t0 = System.currentTimeMillis()
|
||||
@ -861,19 +840,14 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
|
||||
// force the refresh to ensure that the devices list is up-to-date
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
runCatching { deviceListManager.downloadKeys(userIds, true) }
|
||||
.fold(
|
||||
{
|
||||
val unknownDevices = getUnknownDevices(it)
|
||||
if (unknownDevices.map.isEmpty()) {
|
||||
callback.onSuccess(Unit)
|
||||
} else {
|
||||
// trigger an an unknown devices exception
|
||||
callback.onFailure(Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)))
|
||||
}
|
||||
},
|
||||
{ callback.onFailure(it) }
|
||||
)
|
||||
runCatching {
|
||||
val keys = deviceListManager.downloadKeys(userIds, true)
|
||||
val unknownDevices = getUnknownDevices(keys)
|
||||
if (unknownDevices.map.isNotEmpty()) {
|
||||
// trigger an an unknown devices exception
|
||||
throw Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices))
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
@ -27,7 +26,6 @@ import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
// Legacy name: MXDeviceList
|
||||
@ -39,13 +37,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
private val downloadKeysForUsersTask: DownloadKeysForUsersTask) {
|
||||
|
||||
// HS not ready for retry
|
||||
private val notReadyToRetryHS = HashSet<String>()
|
||||
private val notReadyToRetryHS = mutableSetOf<String>()
|
||||
|
||||
init {
|
||||
var isUpdated = false
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
for (userId in deviceTrackingStatuses.keys) {
|
||||
val status = deviceTrackingStatuses[userId]!!
|
||||
for ((userId, status) in deviceTrackingStatuses) {
|
||||
if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) {
|
||||
// if a download was in progress when we got shut down, it isn't any more.
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
||||
@ -66,7 +63,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
private fun canRetryKeysDownload(userId: String): Boolean {
|
||||
var res = false
|
||||
|
||||
if (!TextUtils.isEmpty(userId) && userId.contains(":")) {
|
||||
if (':' in userId) {
|
||||
try {
|
||||
synchronized(notReadyToRetryHS) {
|
||||
res = !notReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1))
|
||||
@ -119,27 +116,23 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
* @param changed the user ids list which have new devices
|
||||
* @param left the user ids list which left a room
|
||||
*/
|
||||
fun handleDeviceListsChanges(changed: List<String>?, left: List<String>?) {
|
||||
fun handleDeviceListsChanges(changed: Collection<String>, left: Collection<String>) {
|
||||
var isUpdated = false
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
|
||||
if (changed?.isNotEmpty() == true) {
|
||||
for (userId in changed) {
|
||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
||||
Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId")
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
||||
isUpdated = true
|
||||
}
|
||||
for (userId in changed) {
|
||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
||||
Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId")
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (left?.isNotEmpty() == true) {
|
||||
for (userId in left) {
|
||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
||||
Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId")
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
|
||||
isUpdated = true
|
||||
}
|
||||
for (userId in left) {
|
||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
||||
Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId")
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,7 +146,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
* + update
|
||||
*/
|
||||
fun invalidateAllDeviceLists() {
|
||||
handleDeviceListsChanges(ArrayList(cryptoStore.getDeviceTrackingStatuses().keys), null)
|
||||
handleDeviceListsChanges(cryptoStore.getDeviceTrackingStatuses().keys, emptyList())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -163,9 +156,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
*/
|
||||
private fun onKeysDownloadFailed(userIds: List<String>) {
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
for (userId in userIds) {
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
||||
}
|
||||
userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD }
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
}
|
||||
|
||||
@ -177,21 +168,15 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
*/
|
||||
private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
if (failures != null) {
|
||||
val keys = failures.keys
|
||||
for (k in keys) {
|
||||
val value = failures[k]
|
||||
if (value!!.containsKey("status")) {
|
||||
val statusCodeAsVoid = value["status"]
|
||||
var statusCode = 0
|
||||
if (statusCodeAsVoid is Double) {
|
||||
statusCode = statusCodeAsVoid.toInt()
|
||||
} else if (statusCodeAsVoid is Int) {
|
||||
statusCode = statusCodeAsVoid.toInt()
|
||||
}
|
||||
if (statusCode == 503) {
|
||||
synchronized(notReadyToRetryHS) {
|
||||
notReadyToRetryHS.add(k)
|
||||
}
|
||||
for ((k, value) in failures) {
|
||||
val statusCode = when (val status = value["status"]) {
|
||||
is Double -> status.toInt()
|
||||
is Int -> status.toInt()
|
||||
else -> 0
|
||||
}
|
||||
if (statusCode == 503) {
|
||||
synchronized(notReadyToRetryHS) {
|
||||
notReadyToRetryHS.add(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -228,11 +213,9 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
/**
|
||||
* Download the device keys for a list of users and stores the keys in the MXStore.
|
||||
* It must be called in getEncryptingThreadHandler() thread.
|
||||
* The callback is called in the UI thread.
|
||||
*
|
||||
* @param userIds The users to fetch.
|
||||
* @param forceDownload Always download the keys even if cached.
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds")
|
||||
@ -270,7 +253,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
Timber.v("## downloadKeys() : starts")
|
||||
val t0 = System.currentTimeMillis()
|
||||
val result = doKeyDownloadForUsers(downloadUsers)
|
||||
Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms")
|
||||
Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms")
|
||||
result.also {
|
||||
it.addEntriesFromMap(stored)
|
||||
}
|
||||
@ -303,16 +286,14 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
val devices = response.deviceKeys?.get(userId)
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices")
|
||||
if (devices != null) {
|
||||
val mutableDevices = HashMap(devices)
|
||||
val deviceIds = ArrayList(mutableDevices.keys)
|
||||
for (deviceId in deviceIds) {
|
||||
val mutableDevices = devices.toMutableMap()
|
||||
for ((deviceId, deviceInfo) in devices) {
|
||||
// Get the potential previously store device keys for this device
|
||||
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
|
||||
val deviceInfo = mutableDevices[deviceId]
|
||||
|
||||
// in some race conditions (like unit tests)
|
||||
// the self device must be seen as verified
|
||||
if (TextUtils.equals(deviceInfo!!.deviceId, credentials.deviceId) && TextUtils.equals(userId, credentials.userId)) {
|
||||
if (deviceInfo.deviceId == credentials.deviceId && userId == credentials.userId) {
|
||||
deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED
|
||||
}
|
||||
// Validate received keys
|
||||
@ -365,13 +346,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
}
|
||||
|
||||
// Check that the user_id and device_id in the received deviceKeys are correct
|
||||
if (!TextUtils.equals(deviceKeys.userId, userId)) {
|
||||
Timber.e("## validateDeviceKeys() : Mismatched user_id " + deviceKeys.userId + " from " + userId + ":" + deviceId)
|
||||
if (deviceKeys.userId != userId) {
|
||||
Timber.e("## validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!TextUtils.equals(deviceKeys.deviceId, deviceId)) {
|
||||
Timber.e("## validateDeviceKeys() : Mismatched device_id " + deviceKeys.deviceId + " from " + userId + ":" + deviceId)
|
||||
if (deviceKeys.deviceId != deviceId) {
|
||||
Timber.e("## validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -379,21 +360,21 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
val signKey = deviceKeys.keys?.get(signKeyId)
|
||||
|
||||
if (null == signKey) {
|
||||
Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no ed25519 key")
|
||||
Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
|
||||
return false
|
||||
}
|
||||
|
||||
val signatureMap = deviceKeys.signatures?.get(userId)
|
||||
|
||||
if (null == signatureMap) {
|
||||
Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no map for " + userId)
|
||||
Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
|
||||
return false
|
||||
}
|
||||
|
||||
val signature = signatureMap[signKeyId]
|
||||
|
||||
if (null == signature) {
|
||||
Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " is not signed")
|
||||
Timber.e("## validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -414,7 +395,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
}
|
||||
|
||||
if (null != previouslyStoredDeviceKeys) {
|
||||
if (!TextUtils.equals(previouslyStoredDeviceKeys.fingerprint(), signKey)) {
|
||||
if (previouslyStoredDeviceKeys.fingerprint() != signKey) {
|
||||
// This should only happen if the list has been MITMed; we are
|
||||
// best off sticking with the original keys.
|
||||
//
|
||||
@ -424,7 +405,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
+ previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey)
|
||||
|
||||
Timber.e("## validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
|
||||
Timber.e("## validateDeviceKeys() : " + previouslyStoredDeviceKeys.keys + " -> " + deviceKeys.keys)
|
||||
Timber.e("## validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
|
||||
|
||||
return false
|
||||
}
|
||||
@ -438,27 +419,18 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
* This method must be called on getEncryptingThreadHandler() thread.
|
||||
*/
|
||||
suspend fun refreshOutdatedDeviceLists() {
|
||||
val users = ArrayList<String>()
|
||||
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
|
||||
for (userId in deviceTrackingStatuses.keys) {
|
||||
if (TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]) {
|
||||
users.add(userId)
|
||||
}
|
||||
val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId ->
|
||||
TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]
|
||||
}
|
||||
|
||||
if (users.size == 0) {
|
||||
if (users.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
// update the statuses
|
||||
for (userId in users) {
|
||||
val status = deviceTrackingStatuses[userId]
|
||||
if (null != status && TRACKING_STATUS_PENDING_DOWNLOAD == status) {
|
||||
deviceTrackingStatuses.put(userId, TRACKING_STATUS_DOWNLOAD_IN_PROGRESS)
|
||||
}
|
||||
}
|
||||
users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS }
|
||||
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
runCatching {
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
@ -25,7 +24,6 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShare
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@ -58,7 +56,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
when (roomKeyShare?.action) {
|
||||
RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(event))
|
||||
RoomKeyShare.ACTION_SHARE_CANCELLATION -> receivedRoomKeyRequestCancellations.add(IncomingRoomKeyRequestCancellation(event))
|
||||
else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action " + roomKeyShare?.action)
|
||||
else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action ${roomKeyShare?.action}")
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +66,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
* It must be called on CryptoThread
|
||||
*/
|
||||
fun processReceivedRoomKeyRequests() {
|
||||
val roomKeyRequestsToProcess = ArrayList(receivedRoomKeyRequests)
|
||||
val roomKeyRequestsToProcess = receivedRoomKeyRequests.toList()
|
||||
receivedRoomKeyRequests.clear()
|
||||
for (request in roomKeyRequestsToProcess) {
|
||||
val userId = request.userId
|
||||
@ -77,7 +75,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
val roomId = body!!.roomId
|
||||
val alg = body.algorithm
|
||||
|
||||
Timber.v("m.room_key_request from " + userId + ":" + deviceId + " for " + roomId + " / " + body.sessionId + " id " + request.requestId)
|
||||
Timber.v("m.room_key_request from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
|
||||
if (userId == null || credentials.userId != userId) {
|
||||
// TODO: determine if we sent this device the keys already: in
|
||||
Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now")
|
||||
@ -92,12 +90,12 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
continue
|
||||
}
|
||||
if (!decryptor.hasKeysForKeyRequest(request)) {
|
||||
Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown session " + body.sessionId!!)
|
||||
Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}")
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
continue
|
||||
}
|
||||
|
||||
if (TextUtils.equals(deviceId, credentials.deviceId) && TextUtils.equals(credentials.userId, userId)) {
|
||||
if (credentials.deviceId == deviceId && credentials.userId == userId) {
|
||||
Timber.v("## processReceivedRoomKeyRequests() : oneself device - ignored")
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
continue
|
||||
@ -132,7 +130,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
var receivedRoomKeyRequestCancellations: List<IncomingRoomKeyRequestCancellation>? = null
|
||||
|
||||
synchronized(this.receivedRoomKeyRequestCancellations) {
|
||||
if (!this.receivedRoomKeyRequestCancellations.isEmpty()) {
|
||||
if (this.receivedRoomKeyRequestCancellations.isNotEmpty()) {
|
||||
receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList()
|
||||
this.receivedRoomKeyRequestCancellations.clear()
|
||||
}
|
||||
|
@ -16,20 +16,19 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.util.Base64
|
||||
import im.vector.matrix.android.internal.extensions.toUnsignedInt
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.experimental.and
|
||||
import kotlin.experimental.xor
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Utility class to import/export the crypto data
|
||||
@ -51,7 +50,7 @@ object MXMegolmExportEncryption {
|
||||
* @return the AES key
|
||||
*/
|
||||
private fun getAesKey(keyBits: ByteArray): ByteArray {
|
||||
return Arrays.copyOfRange(keyBits, 0, 32)
|
||||
return keyBits.copyOfRange(0, 32)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,7 +60,7 @@ object MXMegolmExportEncryption {
|
||||
* @return the Hmac key.
|
||||
*/
|
||||
private fun getHmacKey(keyBits: ByteArray): ByteArray {
|
||||
return Arrays.copyOfRange(keyBits, 32, keyBits.size)
|
||||
return keyBits.copyOfRange(32, keyBits.size)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,7 +76,7 @@ object MXMegolmExportEncryption {
|
||||
val body = unpackMegolmKeyFile(data)
|
||||
|
||||
// check we have a version byte
|
||||
if (null == body || body.size == 0) {
|
||||
if (null == body || body.isEmpty()) {
|
||||
Timber.e("## decryptMegolmKeyFile() : Invalid file: too short")
|
||||
throw Exception("Invalid file: too short")
|
||||
}
|
||||
@ -93,27 +92,27 @@ object MXMegolmExportEncryption {
|
||||
throw Exception("Invalid file: too short")
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
if (password.isEmpty()) {
|
||||
throw Exception("Empty password is not supported")
|
||||
}
|
||||
|
||||
val salt = Arrays.copyOfRange(body, 1, 1 + 16)
|
||||
val iv = Arrays.copyOfRange(body, 17, 17 + 16)
|
||||
val salt = body.copyOfRange(1, 1 + 16)
|
||||
val iv = body.copyOfRange(17, 17 + 16)
|
||||
val iterations =
|
||||
(body[33].toUnsignedInt() shl 24) or (body[34].toUnsignedInt() shl 16) or (body[35].toUnsignedInt() shl 8) or body[36].toUnsignedInt()
|
||||
val ciphertext = Arrays.copyOfRange(body, 37, 37 + ciphertextLength)
|
||||
val hmac = Arrays.copyOfRange(body, body.size - 32, body.size)
|
||||
val ciphertext = body.copyOfRange(37, 37 + ciphertextLength)
|
||||
val hmac = body.copyOfRange(body.size - 32, body.size)
|
||||
|
||||
val deriveKey = deriveKeys(salt, iterations, password)
|
||||
|
||||
val toVerify = Arrays.copyOfRange(body, 0, body.size - 32)
|
||||
val toVerify = body.copyOfRange(0, body.size - 32)
|
||||
|
||||
val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256")
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(macKey)
|
||||
val digest = mac.doFinal(toVerify)
|
||||
|
||||
if (!Arrays.equals(hmac, digest)) {
|
||||
if (!hmac.contentEquals(digest)) {
|
||||
Timber.e("## decryptMegolmKeyFile() : Authentication check failed: incorrect password?")
|
||||
throw Exception("Authentication check failed: incorrect password?")
|
||||
}
|
||||
@ -146,7 +145,7 @@ object MXMegolmExportEncryption {
|
||||
@Throws(Exception::class)
|
||||
@JvmOverloads
|
||||
fun encryptMegolmKeyFile(data: String, password: String, kdf_rounds: Int = DEFAULT_ITERATION_COUNT): ByteArray {
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
if (password.isEmpty()) {
|
||||
throw Exception("Empty password is not supported")
|
||||
}
|
||||
|
||||
@ -196,7 +195,7 @@ object MXMegolmExportEncryption {
|
||||
System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.size)
|
||||
idx += cipherArray.size
|
||||
|
||||
val toSign = Arrays.copyOfRange(resultBuffer, 0, idx)
|
||||
val toSign = resultBuffer.copyOfRange(0, idx)
|
||||
|
||||
val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256")
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
@ -234,7 +233,7 @@ object MXMegolmExportEncryption {
|
||||
// start the next line after the newline
|
||||
lineStart = lineEnd + 1
|
||||
|
||||
if (TextUtils.equals(line, HEADER_LINE)) {
|
||||
if (line == HEADER_LINE) {
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -244,15 +243,13 @@ object MXMegolmExportEncryption {
|
||||
// look for the end line
|
||||
while (true) {
|
||||
val lineEnd = fileStr.indexOf('\n', lineStart)
|
||||
val line: String
|
||||
|
||||
if (lineEnd < 0) {
|
||||
line = fileStr.substring(lineStart).trim()
|
||||
val line = if (lineEnd < 0) {
|
||||
fileStr.substring(lineStart)
|
||||
} else {
|
||||
line = fileStr.substring(lineStart, lineEnd).trim()
|
||||
}
|
||||
fileStr.substring(lineStart, lineEnd)
|
||||
}.trim()
|
||||
|
||||
if (TextUtils.equals(line, TRAILER_LINE)) {
|
||||
if (line == TRAILER_LINE) {
|
||||
break
|
||||
}
|
||||
|
||||
@ -290,7 +287,7 @@ object MXMegolmExportEncryption {
|
||||
for (i in 1..nLines) {
|
||||
outStream.write("\n".toByteArray())
|
||||
|
||||
val len = Math.min(LINE_LENGTH, data.size - o)
|
||||
val len = min(LINE_LENGTH, data.size - o)
|
||||
outStream.write(Base64.encode(data, o, len, Base64.DEFAULT))
|
||||
o += LINE_LENGTH
|
||||
}
|
||||
@ -318,7 +315,7 @@ object MXMegolmExportEncryption {
|
||||
// it is simpler than the generic algorithm because the expected key length is equal to the mac key length.
|
||||
// noticed as dklen/hlen
|
||||
val prf = Mac.getInstance("HmacSHA512")
|
||||
prf.init(SecretKeySpec(password.toByteArray(charset("UTF-8")), "HmacSHA512"))
|
||||
prf.init(SecretKeySpec(password.toByteArray(Charsets.UTF_8), "HmacSHA512"))
|
||||
|
||||
// 512 bits key length
|
||||
val key = ByteArray(64)
|
||||
@ -326,8 +323,7 @@ object MXMegolmExportEncryption {
|
||||
|
||||
// U1 = PRF(Password, Salt || INT_32_BE(i))
|
||||
prf.update(salt)
|
||||
val int32BE = ByteArray(4)
|
||||
Arrays.fill(int32BE, 0.toByte())
|
||||
val int32BE = ByteArray(4) { 0.toByte() }
|
||||
int32BE[3] = 1.toByte()
|
||||
prf.update(int32BE)
|
||||
prf.doFinal(Uc, 0)
|
||||
@ -346,7 +342,7 @@ object MXMegolmExportEncryption {
|
||||
}
|
||||
}
|
||||
|
||||
Timber.v("## deriveKeys() : " + iterations + " in " + (System.currentTimeMillis() - t0) + " ms")
|
||||
Timber.v("## deriveKeys() : $iterations in ${System.currentTimeMillis() - t0} ms")
|
||||
|
||||
return key
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
@ -33,7 +32,6 @@ import im.vector.matrix.android.internal.util.convertToUTF8
|
||||
import org.matrix.olm.*
|
||||
import timber.log.Timber
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
// The libolm wrapper.
|
||||
@ -434,7 +432,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
* @return the base64-encoded secret key.
|
||||
*/
|
||||
fun getSessionKey(sessionId: String): String? {
|
||||
if (!TextUtils.isEmpty(sessionId)) {
|
||||
if (sessionId.isNotEmpty()) {
|
||||
try {
|
||||
return outboundGroupSessionStore[sessionId]!!.sessionKey()
|
||||
} catch (e: Exception) {
|
||||
@ -451,7 +449,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
* @return the current chain index.
|
||||
*/
|
||||
fun getMessageIndex(sessionId: String): Int {
|
||||
return if (!TextUtils.isEmpty(sessionId)) {
|
||||
return if (sessionId.isNotEmpty()) {
|
||||
outboundGroupSessionStore[sessionId]!!.messageIndex()
|
||||
} else 0
|
||||
}
|
||||
@ -464,7 +462,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
* @return ciphertext
|
||||
*/
|
||||
fun encryptGroupMessage(sessionId: String, payloadString: String): String? {
|
||||
if (!TextUtils.isEmpty(sessionId) && !TextUtils.isEmpty(payloadString)) {
|
||||
if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
|
||||
try {
|
||||
return outboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString)
|
||||
} catch (e: Exception) {
|
||||
@ -523,7 +521,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
}
|
||||
|
||||
try {
|
||||
if (!TextUtils.equals(session.olmInboundGroupSession!!.sessionIdentifier(), sessionId)) {
|
||||
if (session.olmInboundGroupSession!!.sessionIdentifier() != sessionId) {
|
||||
Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||
session.olmInboundGroupSession!!.releaseSession()
|
||||
return false
|
||||
@ -573,7 +571,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
}
|
||||
|
||||
try {
|
||||
if (!TextUtils.equals(session.olmInboundGroupSession?.sessionIdentifier(), sessionId)) {
|
||||
if (session.olmInboundGroupSession?.sessionIdentifier() != sessionId) {
|
||||
Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||
if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession()
|
||||
continue
|
||||
@ -758,7 +756,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
if (session != null) {
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
if (!TextUtils.equals(roomId, session.roomId)) {
|
||||
if (roomId != session.roomId) {
|
||||
val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
||||
Timber.e("## getInboundGroupSession() : $errorDescription")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)
|
||||
|
@ -16,12 +16,10 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
@ -42,11 +40,11 @@ internal class MyDeviceInfoHolder @Inject constructor(
|
||||
init {
|
||||
val keys = HashMap<String, String>()
|
||||
|
||||
if (!TextUtils.isEmpty(olmDevice.deviceEd25519Key)) {
|
||||
if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) {
|
||||
keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!!
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(olmDevice.deviceCurve25519Key)) {
|
||||
if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) {
|
||||
keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!!
|
||||
}
|
||||
|
||||
@ -58,13 +56,7 @@ internal class MyDeviceInfoHolder @Inject constructor(
|
||||
// Add our own deviceinfo to the store
|
||||
val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId)
|
||||
|
||||
val myDevices: MutableMap<String, MXDeviceInfo>
|
||||
|
||||
if (null != endToEndDevicesForUser) {
|
||||
myDevices = HashMap(endToEndDevicesForUser)
|
||||
} else {
|
||||
myDevices = HashMap()
|
||||
}
|
||||
val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap()
|
||||
|
||||
myDevices[myDevice.deviceId] = myDevice
|
||||
|
||||
|
@ -24,8 +24,9 @@ import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import org.matrix.olm.OlmAccount
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.min
|
||||
|
||||
@SessionScope
|
||||
internal class OneTimeKeysUploader @Inject constructor(
|
||||
@ -77,7 +78,7 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||
// If we run out of slots when generating new keys then olm will
|
||||
// discard the oldest private keys first. This will eventually clean
|
||||
// out stale private keys that won't receive a message.
|
||||
val keyLimit = Math.floor(maxOneTimeKeys / 2.0).toInt()
|
||||
val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
|
||||
if (oneTimeKeyCount != null) {
|
||||
uploadOTK(oneTimeKeyCount!!, keyLimit)
|
||||
} else {
|
||||
@ -116,7 +117,7 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||
// If we don't need to generate any more keys then we are done.
|
||||
return
|
||||
}
|
||||
val keysThisLoop = Math.min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
|
||||
val keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
|
||||
olmDevice.generateOneTimeKeys(keysThisLoop)
|
||||
val response = uploadOneTimeKeys()
|
||||
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
|
||||
@ -132,14 +133,14 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||
*/
|
||||
private suspend fun uploadOneTimeKeys(): KeysUploadResponse {
|
||||
val oneTimeKeys = olmDevice.getOneTimeKeys()
|
||||
val oneTimeJson = HashMap<String, Any>()
|
||||
val oneTimeJson = mutableMapOf<String, Any>()
|
||||
|
||||
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY)
|
||||
|
||||
if (null != curve25519Map) {
|
||||
for (key_id in curve25519Map.keys) {
|
||||
val k = HashMap<String, Any>()
|
||||
k["key"] = curve25519Map.getValue(key_id)
|
||||
for ((key_id, value) in curve25519Map) {
|
||||
val k = mutableMapOf<String, Any>()
|
||||
k["key"] = value
|
||||
|
||||
// the key is also signed
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
|
||||
|
@ -16,13 +16,11 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
@ -62,10 +60,8 @@ internal class RoomDecryptorProvider @Inject constructor(
|
||||
}
|
||||
if (roomId != null && roomId.isNotEmpty()) {
|
||||
synchronized(roomDecryptors) {
|
||||
if (!roomDecryptors.containsKey(roomId)) {
|
||||
roomDecryptors[roomId] = HashMap()
|
||||
}
|
||||
val alg = roomDecryptors[roomId]?.get(algorithm)
|
||||
val decryptors = roomDecryptors.getOrPut(roomId) { mutableMapOf() }
|
||||
val alg = decryptors[algorithm]
|
||||
if (alg != null) {
|
||||
return alg
|
||||
}
|
||||
@ -89,7 +85,7 @@ internal class RoomDecryptorProvider @Inject constructor(
|
||||
}
|
||||
else -> olmDecryptionFactory.create()
|
||||
}
|
||||
if (roomId != null && !TextUtils.isEmpty(roomId)) {
|
||||
if (!roomId.isNullOrEmpty()) {
|
||||
synchronized(roomDecryptors) {
|
||||
roomDecryptors[roomId]?.put(algorithm, alg)
|
||||
}
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.actions
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXKey
|
||||
@ -24,7 +23,6 @@ import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val olmDevice: MXOlmDevice,
|
||||
@ -35,18 +33,14 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
|
||||
|
||||
val results = MXUsersDevicesMap<MXOlmSessionResult>()
|
||||
|
||||
val userIds = devicesByUser.keys
|
||||
|
||||
for (userId in userIds) {
|
||||
val deviceInfos = devicesByUser[userId]
|
||||
|
||||
for (deviceInfo in deviceInfos!!) {
|
||||
for ((userId, deviceInfos) in devicesByUser) {
|
||||
for (deviceInfo in deviceInfos) {
|
||||
val deviceId = deviceInfo.deviceId
|
||||
val key = deviceInfo.identityKey()
|
||||
|
||||
val sessionId = olmDevice.getSessionId(key!!)
|
||||
|
||||
if (TextUtils.isEmpty(sessionId)) {
|
||||
if (sessionId.isNullOrEmpty()) {
|
||||
devicesWithoutSession.add(deviceInfo)
|
||||
}
|
||||
|
||||
@ -79,9 +73,8 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
|
||||
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
|
||||
val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams)
|
||||
Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
|
||||
for (userId in userIds) {
|
||||
val deviceInfos = devicesByUser[userId]
|
||||
for (deviceInfo in deviceInfos!!) {
|
||||
for ((userId, deviceInfos) in devicesByUser) {
|
||||
for (deviceInfo in deviceInfos) {
|
||||
var oneTimeKey: MXKey? = null
|
||||
val deviceIds = oneTimeKeys.getUserDeviceIds(userId)
|
||||
if (null != deviceIds) {
|
||||
@ -116,24 +109,22 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
|
||||
val signKeyId = "ed25519:$deviceId"
|
||||
val signature = oneTimeKey.signatureForUserId(userId, signKeyId)
|
||||
|
||||
if (!TextUtils.isEmpty(signature) && !TextUtils.isEmpty(deviceInfo.fingerprint())) {
|
||||
if (!signature.isNullOrEmpty() && !deviceInfo.fingerprint().isNullOrEmpty()) {
|
||||
var isVerified = false
|
||||
var errorMessage: String? = null
|
||||
|
||||
if (signature != null) {
|
||||
try {
|
||||
olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature)
|
||||
isVerified = true
|
||||
} catch (e: Exception) {
|
||||
errorMessage = e.message
|
||||
}
|
||||
try {
|
||||
olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature)
|
||||
isVerified = true
|
||||
} catch (e: Exception) {
|
||||
errorMessage = e.message
|
||||
}
|
||||
|
||||
// Check one-time key signature
|
||||
if (isVerified) {
|
||||
sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value)
|
||||
|
||||
if (!TextUtils.isEmpty(sessionId)) {
|
||||
if (!sessionId.isNullOrEmpty()) {
|
||||
Timber.v("## verifyKeyAndStartSession() : Started new sessionid " + sessionId
|
||||
+ " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")")
|
||||
} else {
|
||||
|
@ -16,14 +16,11 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.actions
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val olmDevice: MXOlmDevice,
|
||||
@ -36,27 +33,14 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o
|
||||
*/
|
||||
suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> {
|
||||
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
|
||||
val devicesByUser = HashMap<String /* userId */, MutableList<MXDeviceInfo>>()
|
||||
|
||||
for (userId in users) {
|
||||
devicesByUser[userId] = ArrayList()
|
||||
|
||||
val devicesByUser = users.associateWith { userId ->
|
||||
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
|
||||
|
||||
for (device in devices) {
|
||||
val key = device.identityKey()
|
||||
|
||||
if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) {
|
||||
// Don't bother setting up session to ourself
|
||||
continue
|
||||
}
|
||||
|
||||
if (device.isVerified) {
|
||||
// Don't bother setting up sessions with blocked users
|
||||
continue
|
||||
}
|
||||
|
||||
devicesByUser[userId]!!.add(device)
|
||||
devices.filter {
|
||||
// Don't bother setting up session to ourself
|
||||
it.identityKey() != olmDevice.deviceCurve25519Key
|
||||
// Don't bother setting up sessions with blocked users
|
||||
&& !it.isVerified
|
||||
}
|
||||
}
|
||||
return ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.actions
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
@ -25,7 +24,6 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedMessage
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class MessageEncrypter @Inject constructor(private val credentials: Credentials,
|
||||
@ -40,18 +38,12 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre
|
||||
* @return the content for an m.room.encrypted event.
|
||||
*/
|
||||
fun encryptMessage(payloadFields: Map<String, Any>, deviceInfos: List<MXDeviceInfo>): EncryptedMessage {
|
||||
val deviceInfoParticipantKey = HashMap<String, MXDeviceInfo>()
|
||||
val participantKeys = ArrayList<String>()
|
||||
val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
|
||||
|
||||
for (di in deviceInfos) {
|
||||
participantKeys.add(di.identityKey()!!)
|
||||
deviceInfoParticipantKey[di.identityKey()!!] = di
|
||||
}
|
||||
|
||||
val payloadJson = HashMap(payloadFields)
|
||||
val payloadJson = payloadFields.toMutableMap()
|
||||
|
||||
payloadJson["sender"] = credentials.userId
|
||||
payloadJson["sender_device"] = credentials.deviceId
|
||||
payloadJson["sender_device"] = credentials.deviceId!!
|
||||
|
||||
// Include the Ed25519 key so that the recipient knows what
|
||||
// device this message came from.
|
||||
@ -67,30 +59,24 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre
|
||||
|
||||
val ciphertext = HashMap<String, Any>()
|
||||
|
||||
for (deviceKey in participantKeys) {
|
||||
for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) {
|
||||
val sessionId = olmDevice.getSessionId(deviceKey)
|
||||
|
||||
if (!TextUtils.isEmpty(sessionId)) {
|
||||
if (!sessionId.isNullOrEmpty()) {
|
||||
Timber.v("Using sessionid $sessionId for device $deviceKey")
|
||||
val deviceInfo = deviceInfoParticipantKey[deviceKey]
|
||||
|
||||
payloadJson["recipient"] = deviceInfo!!.userId
|
||||
|
||||
val recipientsKeysMap = HashMap<String, String>()
|
||||
recipientsKeysMap["ed25519"] = deviceInfo.fingerprint()!!
|
||||
payloadJson["recipient_keys"] = recipientsKeysMap
|
||||
payloadJson["recipient"] = deviceInfo.userId
|
||||
payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!)
|
||||
|
||||
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
|
||||
ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId!!, payloadString)!!
|
||||
ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId, payloadString)!!
|
||||
}
|
||||
}
|
||||
|
||||
val res = EncryptedMessage()
|
||||
|
||||
res.algorithm = MXCRYPTO_ALGORITHM_OLM
|
||||
res.senderKey = olmDevice.deviceCurve25519Key
|
||||
res.cipherText = ciphertext
|
||||
|
||||
return res
|
||||
return EncryptedMessage(
|
||||
algorithm = MXCRYPTO_ALGORITHM_OLM,
|
||||
senderKey = olmDevice.deviceCurve25519Key,
|
||||
cipherText = ciphertext
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
@ -148,7 +147,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
selfMap["deviceId"] = "*"
|
||||
recipients.add(selfMap)
|
||||
|
||||
if (!TextUtils.equals(sender, userId)) {
|
||||
if (sender != userId) {
|
||||
val senderMap = HashMap<String, String>()
|
||||
senderMap["userId"] = sender
|
||||
senderMap["deviceId"] = encryptedEventContent.deviceId!!
|
||||
@ -176,17 +175,12 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
|
||||
val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
|
||||
|
||||
if (!pendingEvents.containsKey(pendingEventsKey)) {
|
||||
pendingEvents[pendingEventsKey] = HashMap()
|
||||
}
|
||||
val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
|
||||
val events = timeline.getOrPut(timelineId) { ArrayList() }
|
||||
|
||||
if (pendingEvents[pendingEventsKey]?.containsKey(timelineId) == false) {
|
||||
pendingEvents[pendingEventsKey]?.put(timelineId, ArrayList())
|
||||
}
|
||||
|
||||
if (pendingEvents[pendingEventsKey]?.get(timelineId)?.contains(event) == false) {
|
||||
Timber.v("## addEventToPendingList() : add Event " + event.eventId + " in room id " + event.roomId)
|
||||
pendingEvents[pendingEventsKey]?.get(timelineId)?.add(event)
|
||||
if (event !in events) {
|
||||
Timber.v("## addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
|
||||
events.add(event)
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,7 +197,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
var keysClaimed: MutableMap<String, String> = HashMap()
|
||||
val forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
|
||||
|
||||
if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.sessionId) || TextUtils.isEmpty(roomKeyContent.sessionKey)) {
|
||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) {
|
||||
Timber.e("## onRoomKeyEvent() : Key event is missing fields")
|
||||
return
|
||||
}
|
||||
@ -250,13 +244,6 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||
keysClaimed = event.getKeysClaimed().toMutableMap()
|
||||
}
|
||||
|
||||
if (roomKeyContent.sessionId == null
|
||||
|| roomKeyContent.sessionKey == null
|
||||
|| roomKeyContent.roomId == null) {
|
||||
Timber.e("## invalid roomKeyContent")
|
||||
return
|
||||
}
|
||||
|
||||
val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId,
|
||||
roomKeyContent.sessionKey,
|
||||
roomKeyContent.roomId,
|
||||
|
@ -18,7 +18,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
@ -38,7 +37,6 @@ import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class MXMegolmEncryption(
|
||||
// The id of the room we will be sending to.
|
||||
@ -85,7 +83,7 @@ internal class MXMegolmEncryption(
|
||||
keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!!
|
||||
|
||||
olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
|
||||
ArrayList(), keysClaimedMap, false)
|
||||
emptyList(), keysClaimedMap, false)
|
||||
|
||||
keysBackup.maybeBackupKeys()
|
||||
|
||||
@ -115,10 +113,8 @@ internal class MXMegolmEncryption(
|
||||
for (deviceId in deviceIds!!) {
|
||||
val deviceInfo = devicesInRoom.getObject(userId, deviceId)
|
||||
if (deviceInfo != null && null == safeSession.sharedWithDevices.getObject(userId, deviceId)) {
|
||||
if (!shareMap.containsKey(userId)) {
|
||||
shareMap[userId] = ArrayList()
|
||||
}
|
||||
shareMap[userId]!!.add(deviceInfo)
|
||||
val devices = shareMap.getOrPut(userId) { ArrayList() }
|
||||
devices.add(deviceInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,21 +137,17 @@ internal class MXMegolmEncryption(
|
||||
}
|
||||
// reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user)
|
||||
val subMap = HashMap<String, List<MXDeviceInfo>>()
|
||||
val userIds = ArrayList<String>()
|
||||
var devicesCount = 0
|
||||
for (userId in devicesByUsers.keys) {
|
||||
devicesByUsers[userId]?.let {
|
||||
userIds.add(userId)
|
||||
subMap[userId] = it
|
||||
devicesCount += it.size
|
||||
}
|
||||
for ((userId, devices) in devicesByUsers) {
|
||||
subMap[userId] = devices
|
||||
devicesCount += devices.size
|
||||
if (devicesCount > 100) {
|
||||
break
|
||||
}
|
||||
}
|
||||
Timber.v("## shareKey() ; userId $userIds")
|
||||
Timber.v("## shareKey() ; userId ${subMap.keys}")
|
||||
shareUserDevicesKey(session, subMap)
|
||||
val remainingDevices = devicesByUsers.filterKeys { userIds.contains(it).not() }
|
||||
val remainingDevices = devicesByUsers - subMap.keys
|
||||
shareKey(session, remainingDevices)
|
||||
}
|
||||
|
||||
@ -164,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>>) {
|
||||
@ -210,8 +201,7 @@ internal class MXMegolmEncryption(
|
||||
continue
|
||||
}
|
||||
Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
|
||||
//noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument
|
||||
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo)))
|
||||
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo)))
|
||||
haveTargets = true
|
||||
}
|
||||
}
|
||||
@ -228,9 +218,8 @@ internal class MXMegolmEncryption(
|
||||
// attempted to share with) rather than the contentMap (those we did
|
||||
// share with), because we don't want to try to claim a one-time-key
|
||||
// for dead devices on every message.
|
||||
for (userId in devicesByUser.keys) {
|
||||
val devicesToShareWith = devicesByUser[userId]
|
||||
for ((deviceId) in devicesToShareWith!!) {
|
||||
for ((userId, devicesToShareWith) in devicesByUser) {
|
||||
for ((deviceId) in devicesToShareWith) {
|
||||
session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
|
||||
}
|
||||
}
|
||||
@ -272,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
|
||||
@ -304,7 +292,7 @@ internal class MXMegolmEncryption(
|
||||
continue
|
||||
}
|
||||
|
||||
if (TextUtils.equals(deviceInfo.identityKey(), olmDevice.deviceCurve25519Key)) {
|
||||
if (deviceInfo.identityKey() == olmDevice.deviceCurve25519Key) {
|
||||
// Don't bother sending to ourself
|
||||
continue
|
||||
}
|
||||
|
@ -18,7 +18,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms.olm
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||
@ -28,7 +27,6 @@ import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import java.util.*
|
||||
|
||||
internal class MXOlmEncryption(
|
||||
private var roomId: String,
|
||||
@ -49,7 +47,7 @@ internal class MXOlmEncryption(
|
||||
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
|
||||
for (device in devices) {
|
||||
val key = device.identityKey()
|
||||
if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) {
|
||||
if (key == olmDevice.deviceCurve25519Key) {
|
||||
// Don't bother setting up session to ourself
|
||||
continue
|
||||
}
|
||||
@ -61,13 +59,14 @@ internal class MXOlmEncryption(
|
||||
}
|
||||
}
|
||||
|
||||
val messageMap = HashMap<String, Any>()
|
||||
messageMap["room_id"] = roomId
|
||||
messageMap["type"] = eventType
|
||||
messageMap["content"] = eventContent
|
||||
val messageMap = mapOf(
|
||||
"room_id" to roomId,
|
||||
"type" to eventType,
|
||||
"content" to eventContent
|
||||
)
|
||||
|
||||
messageEncrypter.encryptMessage(messageMap, deviceInfos)
|
||||
return messageMap.toContent()!!
|
||||
return messageMap.toContent()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -21,7 +21,6 @@ import android.os.Looper
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
@ -50,6 +49,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
|
||||
@ -58,6 +58,7 @@ import im.vector.matrix.android.internal.task.TaskThread
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.util.awaitCallback
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -77,6 +78,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,
|
||||
@ -142,8 +144,8 @@ internal class KeysBackup @Inject constructor(
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
Try {
|
||||
runCatching {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
val olmPkDecryption = OlmPkDecryption()
|
||||
val megolmBackupAuthData = MegolmBackupAuthData()
|
||||
|
||||
@ -375,8 +377,6 @@ internal class KeysBackup @Inject constructor(
|
||||
*/
|
||||
@WorkerThread
|
||||
private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
|
||||
val myUserId = credentials.userId
|
||||
|
||||
val keysBackupVersionTrust = KeysBackupVersionTrust()
|
||||
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
|
||||
|
||||
@ -388,13 +388,13 @@ 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
|
||||
}
|
||||
|
||||
for (keyId in mySigs.keys) {
|
||||
for ((keyId, mySignature) in mySigs) {
|
||||
// XXX: is this how we're supposed to get the device id?
|
||||
var deviceId: String? = null
|
||||
val components = keyId.split(":")
|
||||
@ -403,7 +403,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) {
|
||||
@ -412,7 +412,7 @@ internal class KeysBackup @Inject constructor(
|
||||
val fingerprint = device.fingerprint()
|
||||
if (fingerprint != null) {
|
||||
try {
|
||||
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySigs[keyId] as String)
|
||||
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature)
|
||||
isSignatureValid = true
|
||||
} catch (e: OlmException) {
|
||||
Timber.v(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}")
|
||||
@ -450,10 +450,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) {
|
||||
@ -462,7 +460,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 {
|
||||
@ -478,7 +476,7 @@ internal class KeysBackup @Inject constructor(
|
||||
val newMegolmBackupAuthData = authData.copy()
|
||||
|
||||
val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap()
|
||||
newSignatures[myUserId] = myUserSignatures
|
||||
newSignatures[userId] = myUserSignatures
|
||||
|
||||
newMegolmBackupAuthData.signatures = newSignatures
|
||||
|
||||
@ -617,8 +615,8 @@ internal class KeysBackup @Inject constructor(
|
||||
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
||||
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
Try<OlmPkDecryption> {
|
||||
runCatching {
|
||||
val decryption = withContext(coroutineDispatchers.crypto) {
|
||||
// Check if the recovery is valid before going any further
|
||||
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
|
||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
||||
@ -626,85 +624,66 @@ internal class KeysBackup @Inject constructor(
|
||||
}
|
||||
|
||||
// Get a PK decryption instance
|
||||
val decryption = pkDecryptionFromRecoveryKey(recoveryKey)
|
||||
if (decryption == null) {
|
||||
// This should not happen anymore
|
||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
|
||||
throw InvalidParameterException("Invalid recovery key")
|
||||
}
|
||||
|
||||
decryption
|
||||
pkDecryptionFromRecoveryKey(recoveryKey)
|
||||
}
|
||||
if (decryption == null) {
|
||||
// This should not happen anymore
|
||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
|
||||
throw InvalidParameterException("Invalid recovery key")
|
||||
}
|
||||
}.fold(
|
||||
{
|
||||
callback.onFailure(it)
|
||||
},
|
||||
{ decryption ->
|
||||
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
|
||||
|
||||
// Get backed up keys from the homeserver
|
||||
getKeys(sessionId, roomId, keysVersionResult.version!!, object : MatrixCallback<KeysBackupData> {
|
||||
override fun onSuccess(data: KeysBackupData) {
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
val importRoomKeysResult = withContext(coroutineDispatchers.crypto) {
|
||||
val sessionsData = ArrayList<MegolmSessionData>()
|
||||
// Restore that data
|
||||
var sessionsFromHsCount = 0
|
||||
for (roomIdLoop in data.roomIdToRoomKeysBackupData.keys) {
|
||||
for (sessionIdLoop in data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData.keys) {
|
||||
sessionsFromHsCount++
|
||||
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
|
||||
|
||||
val keyBackupData = data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData[sessionIdLoop]!!
|
||||
// Get backed up keys from the homeserver
|
||||
val data = getKeys(sessionId, roomId, keysVersionResult.version!!)
|
||||
|
||||
val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption)
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
val sessionsData = ArrayList<MegolmSessionData>()
|
||||
// Restore that data
|
||||
var sessionsFromHsCount = 0
|
||||
for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) {
|
||||
for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) {
|
||||
sessionsFromHsCount++
|
||||
|
||||
sessionData?.let {
|
||||
sessionsData.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
Timber.v("restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" +
|
||||
" of $sessionsFromHsCount from the backup store on the homeserver")
|
||||
val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption)
|
||||
|
||||
// Do not trigger a backup for them if they come from the backup version we are using
|
||||
val backUp = keysVersionResult.version != keysBackupVersion?.version
|
||||
if (backUp) {
|
||||
Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up" +
|
||||
" to backup version: ${keysBackupVersion?.version}")
|
||||
}
|
||||
|
||||
// Import them into the crypto store
|
||||
val progressListener = if (stepProgressListener != null) {
|
||||
object : ProgressListener {
|
||||
override fun onProgress(progress: Int, total: Int) {
|
||||
// Note: no need to post to UI thread, importMegolmSessionsData() will do it
|
||||
stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val result = megolmSessionDataImporter.handle(sessionsData, !backUp, uiHandler, progressListener)
|
||||
|
||||
// Do not back up the key if it comes from a backup recovery
|
||||
if (backUp) {
|
||||
maybeBackupKeys()
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
callback.onSuccess(importRoomKeysResult)
|
||||
}
|
||||
sessionData?.let {
|
||||
sessionsData.add(it)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
Timber.v("restoreKeysWithRecoveryKey: Decrypted ${sessionsData.size} keys out" +
|
||||
" of $sessionsFromHsCount from the backup store on the homeserver")
|
||||
|
||||
// Do not trigger a backup for them if they come from the backup version we are using
|
||||
val backUp = keysVersionResult.version != keysBackupVersion?.version
|
||||
if (backUp) {
|
||||
Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up" +
|
||||
" to backup version: ${keysBackupVersion?.version}")
|
||||
}
|
||||
|
||||
// Import them into the crypto store
|
||||
val progressListener = if (stepProgressListener != null) {
|
||||
object : ProgressListener {
|
||||
override fun onProgress(progress: Int, total: Int) {
|
||||
// Note: no need to post to UI thread, importMegolmSessionsData() will do it
|
||||
stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val result = megolmSessionDataImporter.handle(sessionsData, !backUp, uiHandler, progressListener)
|
||||
|
||||
// Do not back up the key if it comes from a backup recovery
|
||||
if (backUp) {
|
||||
maybeBackupKeys()
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
@ -717,7 +696,7 @@ internal class KeysBackup @Inject constructor(
|
||||
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
|
||||
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
runCatching {
|
||||
val progressListener = if (stepProgressListener != null) {
|
||||
object : ProgressListener {
|
||||
override fun onProgress(progress: Int, total: Int) {
|
||||
@ -730,22 +709,18 @@ internal class KeysBackup @Inject constructor(
|
||||
null
|
||||
}
|
||||
|
||||
Try {
|
||||
val recoveryKey = withContext(coroutineDispatchers.crypto) {
|
||||
recoveryKeyFromPassword(password, keysBackupVersion, progressListener)
|
||||
}
|
||||
}.fold(
|
||||
{
|
||||
callback.onFailure(it)
|
||||
},
|
||||
{ recoveryKey ->
|
||||
if (recoveryKey == null) {
|
||||
Timber.v("backupKeys: Invalid configuration")
|
||||
callback.onFailure(IllegalStateException("Invalid configuration"))
|
||||
} else {
|
||||
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, callback)
|
||||
}
|
||||
if (recoveryKey == null) {
|
||||
Timber.v("backupKeys: Invalid configuration")
|
||||
throw IllegalStateException("Invalid configuration")
|
||||
} else {
|
||||
awaitCallback<ImportRoomKeysResult> {
|
||||
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
@ -753,60 +728,26 @@ internal class KeysBackup @Inject constructor(
|
||||
* Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable
|
||||
* parameters and always returns a KeysBackupData object through the Callback
|
||||
*/
|
||||
private fun getKeys(sessionId: String?,
|
||||
private suspend fun getKeys(sessionId: String?,
|
||||
roomId: String?,
|
||||
version: String,
|
||||
callback: MatrixCallback<KeysBackupData>) {
|
||||
if (roomId != null && sessionId != null) {
|
||||
version: String): KeysBackupData {
|
||||
return if (roomId != null && sessionId != null) {
|
||||
// Get key for the room and for the session
|
||||
getRoomSessionDataTask
|
||||
.configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version)) {
|
||||
this.callback = object : MatrixCallback<KeyBackupData> {
|
||||
override fun onSuccess(data: KeyBackupData) {
|
||||
// Convert to KeysBackupData
|
||||
val keysBackupData = KeysBackupData()
|
||||
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
|
||||
val roomKeysBackupData = RoomKeysBackupData()
|
||||
roomKeysBackupData.sessionIdToKeyBackupData = HashMap()
|
||||
roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data
|
||||
keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData
|
||||
|
||||
callback.onSuccess(keysBackupData)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version))
|
||||
// Convert to KeysBackupData
|
||||
KeysBackupData(mutableMapOf(
|
||||
roomId to RoomKeysBackupData(mutableMapOf(
|
||||
sessionId to data
|
||||
))
|
||||
))
|
||||
} else if (roomId != null) {
|
||||
// Get all keys for the room
|
||||
getRoomSessionsDataTask
|
||||
.configureWith(GetRoomSessionsDataTask.Params(roomId, version)) {
|
||||
this.callback = object : MatrixCallback<RoomKeysBackupData> {
|
||||
override fun onSuccess(data: RoomKeysBackupData) {
|
||||
// Convert to KeysBackupData
|
||||
val keysBackupData = KeysBackupData()
|
||||
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
|
||||
keysBackupData.roomIdToRoomKeysBackupData[roomId] = data
|
||||
|
||||
callback.onSuccess(keysBackupData)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version))
|
||||
// Convert to KeysBackupData
|
||||
KeysBackupData(mutableMapOf(roomId to data))
|
||||
} else {
|
||||
// Get all keys
|
||||
getSessionsDataTask
|
||||
.configureWith(GetSessionsDataTask.Params(version)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
getSessionsDataTask.execute(GetSessionsDataTask.Params(version))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1411,5 +1352,5 @@ internal class KeysBackup @Inject constructor(
|
||||
* DEBUG INFO
|
||||
* ========================================================================================== */
|
||||
|
||||
override fun toString() = "KeysBackup for ${credentials.userId}"
|
||||
override fun toString() = "KeysBackup for $userId"
|
||||
}
|
||||
|
@ -17,13 +17,11 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.model
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
||||
import org.matrix.olm.OlmInboundGroupSession
|
||||
import timber.log.Timber
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This class adds more context to a OlmInboundGroupSession object.
|
||||
@ -91,7 +89,7 @@ class OlmInboundGroupSessionWrapper : Serializable {
|
||||
try {
|
||||
olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!)
|
||||
|
||||
if (!TextUtils.equals(olmInboundGroupSession!!.sessionIdentifier(), megolmSessionData.sessionId)) {
|
||||
if (olmInboundGroupSession!!.sessionIdentifier() != megolmSessionData.sessionId) {
|
||||
throw Exception("Mismatched group session Id")
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.store.db
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||
@ -101,8 +100,8 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
|
||||
// Check credentials
|
||||
// The device id may not have been provided in credentials.
|
||||
// Check it only if provided, else trust the stored one.
|
||||
if (!TextUtils.equals(currentMetadata.userId, credentials.userId)
|
||||
|| (credentials.deviceId != null && !TextUtils.equals(credentials.deviceId, currentMetadata.deviceId))) {
|
||||
if (currentMetadata.userId != credentials.userId
|
||||
|| (credentials.deviceId != null && credentials.deviceId != currentMetadata.deviceId)) {
|
||||
Timber.w("## open() : Credentials do not match, close this store and delete data")
|
||||
deleteAll = true
|
||||
currentMetadata = null
|
||||
|
@ -44,18 +44,14 @@ internal class DefaultClaimOneTimeKeysForUsersDevice @Inject constructor(private
|
||||
}
|
||||
val map = MXUsersDevicesMap<MXKey>()
|
||||
keysClaimResponse.oneTimeKeys?.let { oneTimeKeys ->
|
||||
for (userId in oneTimeKeys.keys) {
|
||||
val mapByUserId = oneTimeKeys[userId]
|
||||
for ((userId, mapByUserId) in oneTimeKeys) {
|
||||
for ((deviceId, deviceKey) in mapByUserId) {
|
||||
val mxKey = MXKey.from(deviceKey)
|
||||
|
||||
if (mapByUserId != null) {
|
||||
for (deviceId in mapByUserId.keys) {
|
||||
val mxKey = MXKey.from(mapByUserId[deviceId])
|
||||
|
||||
if (mxKey != null) {
|
||||
map.setObject(userId, deviceId, mxKey)
|
||||
} else {
|
||||
Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey")
|
||||
}
|
||||
if (mxKey != null) {
|
||||
map.setObject(userId, deviceId, mxKey)
|
||||
} else {
|
||||
Timber.e("## claimOneTimeKeysForUsersDevices : fail to create a MXKey")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,13 +16,11 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.tasks
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryBody
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryResponse
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface DownloadKeysForUsersTask : Task<DownloadKeysForUsersTask.Params, KeysQueryResponse> {
|
||||
@ -37,19 +35,13 @@ internal class DefaultDownloadKeysForUsers @Inject constructor(private val crypt
|
||||
: DownloadKeysForUsersTask {
|
||||
|
||||
override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse {
|
||||
val downloadQuery = HashMap<String, Map<String, Any>>()
|
||||
|
||||
if (null != params.userIds) {
|
||||
for (userId in params.userIds) {
|
||||
downloadQuery[userId] = HashMap()
|
||||
}
|
||||
}
|
||||
val downloadQuery = params.userIds?.associateWith { emptyMap<String, Any>() }.orEmpty()
|
||||
|
||||
val body = KeysQueryBody(
|
||||
deviceKeys = downloadQuery
|
||||
)
|
||||
|
||||
if (!TextUtils.isEmpty(params.token)) {
|
||||
if (!params.token.isNullOrEmpty()) {
|
||||
body.token = params.token
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.tasks
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UpdateDeviceInfoBody
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
@ -37,7 +36,7 @@ internal class DefaultSetDeviceNameTask @Inject constructor(private val cryptoAp
|
||||
|
||||
override suspend fun execute(params: SetDeviceNameTask.Params) {
|
||||
val body = UpdateDeviceInfoBody(
|
||||
displayName = if (TextUtils.isEmpty(params.deviceName)) "" else params.deviceName
|
||||
displayName = params.deviceName
|
||||
)
|
||||
return executeRequest {
|
||||
apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body)
|
||||
|
@ -124,7 +124,7 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
backwardsDisplayIndex = currentDisplayIndex
|
||||
}
|
||||
var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset)
|
||||
if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.getClearType())) {
|
||||
if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) {
|
||||
currentStateIndex += 1
|
||||
forwardsStateIndex = currentStateIndex
|
||||
} else if (direction == PaginationDirection.BACKWARDS && timelineEvents.isNotEmpty()) {
|
||||
|
@ -37,7 +37,7 @@ internal object EventMapper {
|
||||
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
|
||||
eventEntity.prevContent = ContentMapper.map(resolvedPrevContent)
|
||||
eventEntity.stateKey = event.stateKey
|
||||
eventEntity.type = event.getClearType()
|
||||
eventEntity.type = event.type
|
||||
eventEntity.sender = event.senderId
|
||||
eventEntity.originServerTs = event.originServerTs
|
||||
eventEntity.redacts = event.redacts
|
||||
|
@ -17,7 +17,6 @@
|
||||
package im.vector.matrix.android.internal.network
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.internal.di.MatrixScope
|
||||
import timber.log.Timber
|
||||
@ -60,10 +59,10 @@ internal class UserAgentHolder @Inject constructor(private val context: Context)
|
||||
Timber.e(e, "## initUserAgent() : failed")
|
||||
}
|
||||
|
||||
var systemUserAgent = System.getProperty("http.agent")
|
||||
val systemUserAgent = System.getProperty("http.agent")
|
||||
|
||||
// cannot retrieve the application version
|
||||
if (TextUtils.isEmpty(appName) || TextUtils.isEmpty(appVersion)) {
|
||||
if (appName.isEmpty() || appVersion.isEmpty()) {
|
||||
if (null == systemUserAgent) {
|
||||
userAgent = "Java" + System.getProperty("java.version")
|
||||
}
|
||||
|
@ -75,9 +75,7 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
|
||||
private fun updateState(key: String, state: ContentUploadStateTracker.State) {
|
||||
states[key] = state
|
||||
mainHandler.post {
|
||||
listeners[key]?.also { listeners ->
|
||||
listeners.forEach { it.onUpdate(state) }
|
||||
}
|
||||
listeners[key]?.forEach { it.onUpdate(state) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,13 +65,11 @@ internal class DefaultGetGroupDataTask @Inject constructor(
|
||||
groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name
|
||||
groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription ?: ""
|
||||
|
||||
val roomIds = groupRooms.rooms.map { it.roomId }
|
||||
groupSummaryEntity.roomIds.clear()
|
||||
groupSummaryEntity.roomIds.addAll(roomIds)
|
||||
groupRooms.rooms.mapTo(groupSummaryEntity.roomIds) { it.roomId }
|
||||
|
||||
val userIds = groupUsers.users.map { it.userId }
|
||||
groupSummaryEntity.userIds.clear()
|
||||
groupSummaryEntity.userIds.addAll(userIds)
|
||||
groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId }
|
||||
|
||||
groupSummaryEntity.membership = when (groupSummary.user?.membership) {
|
||||
Membership.JOIN.value -> Membership.JOIN
|
||||
|
@ -50,19 +50,15 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
||||
defaultPushRuleService.dispatchRoomJoined(it)
|
||||
}
|
||||
val newJoinEvents = params.syncResponse.join
|
||||
.map { entries ->
|
||||
entries.value.timeline?.events?.map { it.copy(roomId = entries.key) }
|
||||
.mapNotNull { (key, value) ->
|
||||
value.timeline?.events?.map { it.copy(roomId = key) }
|
||||
}
|
||||
.fold(emptyList<Event>(), { acc, next ->
|
||||
acc + (next ?: emptyList())
|
||||
})
|
||||
.flatten()
|
||||
val inviteEvents = params.syncResponse.invite
|
||||
.map { entries ->
|
||||
entries.value.inviteState?.events?.map { it.copy(roomId = entries.key) }
|
||||
.mapNotNull { (key, value) ->
|
||||
value.inviteState?.events?.map { it.copy(roomId = key) }
|
||||
}
|
||||
.fold(emptyList<Event>(), { acc, next ->
|
||||
acc + (next ?: emptyList())
|
||||
})
|
||||
.flatten()
|
||||
val allEvents = (newJoinEvents + inviteEvents).filter { event ->
|
||||
when (event.type) {
|
||||
EventType.MESSAGE,
|
||||
@ -84,16 +80,12 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
||||
}
|
||||
|
||||
val allRedactedEvents = params.syncResponse.join
|
||||
.map { entries ->
|
||||
entries.value.timeline?.events?.filter {
|
||||
it.type == EventType.REDACTION
|
||||
}
|
||||
.orEmpty()
|
||||
.mapNotNull { it.redacts }
|
||||
}
|
||||
.fold(emptyList<String>(), { acc, next ->
|
||||
acc + next
|
||||
})
|
||||
.asSequence()
|
||||
.mapNotNull { (_, value) -> value.timeline?.events }
|
||||
.flatten()
|
||||
.filter { it.type == EventType.REDACTION }
|
||||
.mapNotNull { it.redacts }
|
||||
.toList()
|
||||
|
||||
Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events")
|
||||
|
||||
@ -107,18 +99,11 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
||||
private fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? {
|
||||
// TODO This should be injected
|
||||
val conditionResolver = DefaultConditionResolver(event, roomService, userId)
|
||||
rules.filter { it.enabled }.forEach { rule ->
|
||||
val isFullfilled = rule.conditions?.map {
|
||||
return rules.firstOrNull { rule ->
|
||||
// All conditions must hold true for an event in order to apply the action for the event.
|
||||
rule.enabled && rule.conditions?.all {
|
||||
it.asExecutableCondition()?.isSatisfied(conditionResolver) ?: false
|
||||
}?.fold(true/*A rule with no conditions always matches*/, { acc, next ->
|
||||
// All conditions must hold true for an event in order to apply the action for the event.
|
||||
acc && next
|
||||
}) ?: false
|
||||
|
||||
if (isFullfilled) {
|
||||
return rule
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -132,8 +132,7 @@ internal class RoomMembers(private val realm: Realm,
|
||||
.findAll()
|
||||
.map { it.asDomain() }
|
||||
.associateBy { it.stateKey!! }
|
||||
.mapValues { it.value.content.toModel<RoomMember>()!! }
|
||||
.filterValues { predicate(it) }
|
||||
.filterValues { predicate(it.content.toModel<RoomMember>()!!) }
|
||||
.keys
|
||||
.toList()
|
||||
}
|
||||
|
@ -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,33 +38,33 @@ 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? {
|
||||
val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||
if (summary != null) {
|
||||
summary.reactionsSummary.where()
|
||||
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction)
|
||||
.findFirst()?.let {
|
||||
// want to find the event orignated by me!
|
||||
it.sourceEvents.forEach {
|
||||
// find source event
|
||||
EventEntity.where(realm, it).findFirst()?.let { eventEntity ->
|
||||
// is it mine?
|
||||
if (eventEntity.sender == userId) {
|
||||
return eventEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String): EventEntity? {
|
||||
val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() ?: return null
|
||||
|
||||
val rase = summary.reactionsSummary.where()
|
||||
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction)
|
||||
.findFirst() ?: return null
|
||||
|
||||
// want to find the event orignated by me!
|
||||
return rase.sourceEvents
|
||||
.asSequence()
|
||||
.mapNotNull {
|
||||
// find source event
|
||||
EventEntity.where(realm, it).findFirst()
|
||||
}
|
||||
.firstOrNull { eventEntity ->
|
||||
// is it mine?
|
||||
eventEntity.sender == userId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -38,24 +38,24 @@ internal class TimelineEventDecryptor(
|
||||
private val newSessionListener = object : NewSessionListener {
|
||||
override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
|
||||
synchronized(unknownSessionsFailure) {
|
||||
val toDecryptAgain = ArrayList<String>()
|
||||
unknownSessionsFailure[sessionId]?.let { eventIds ->
|
||||
toDecryptAgain.addAll(eventIds)
|
||||
}
|
||||
if (toDecryptAgain.isNotEmpty()) {
|
||||
unknownSessionsFailure[sessionId]?.clear()
|
||||
toDecryptAgain.forEach {
|
||||
requestDecryption(it)
|
||||
}
|
||||
}
|
||||
unknownSessionsFailure[sessionId]
|
||||
.orEmpty()
|
||||
.toList()
|
||||
.also {
|
||||
unknownSessionsFailure[sessionId]?.clear()
|
||||
}
|
||||
}.forEach {
|
||||
requestDecryption(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var executor: ExecutorService? = null
|
||||
|
||||
private val existingRequests = HashSet<String>()
|
||||
private val unknownSessionsFailure = HashMap<String, MutableList<String>>()
|
||||
// Set of eventIds which are currently decrypting
|
||||
private val existingRequests = mutableSetOf<String>()
|
||||
// sessionId -> list of eventIds
|
||||
private val unknownSessionsFailure = mutableMapOf<String, MutableList<String>>()
|
||||
|
||||
fun start() {
|
||||
executor = Executors.newSingleThreadExecutor()
|
||||
@ -66,26 +66,30 @@ internal class TimelineEventDecryptor(
|
||||
cryptoService.removeSessionListener(newSessionListener)
|
||||
executor?.shutdownNow()
|
||||
executor = null
|
||||
unknownSessionsFailure.clear()
|
||||
existingRequests.clear()
|
||||
synchronized(unknownSessionsFailure) {
|
||||
unknownSessionsFailure.clear()
|
||||
}
|
||||
synchronized(existingRequests) {
|
||||
existingRequests.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun requestDecryption(eventId: String) {
|
||||
synchronized(existingRequests) {
|
||||
if (existingRequests.contains(eventId)) {
|
||||
return Unit.also {
|
||||
Timber.d("Skip Decryption request for event $eventId, already requested")
|
||||
synchronized(unknownSessionsFailure) {
|
||||
for (eventIds in unknownSessionsFailure.values) {
|
||||
if (eventId in eventIds) {
|
||||
Timber.d("Skip Decryption request for event $eventId, unknown session")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
synchronized(existingRequests) {
|
||||
if (eventId in existingRequests) {
|
||||
Timber.d("Skip Decryption request for event $eventId, already requested")
|
||||
return
|
||||
}
|
||||
existingRequests.add(eventId)
|
||||
}
|
||||
synchronized(unknownSessionsFailure) {
|
||||
unknownSessionsFailure.values.forEach {
|
||||
if (it.contains(eventId)) return@synchronized Unit.also {
|
||||
Timber.d("Skip Decryption request for event $eventId, unknown session")
|
||||
}
|
||||
}
|
||||
}
|
||||
executor?.execute {
|
||||
Realm.getInstance(realmConfiguration).use { realm ->
|
||||
processDecryptRequest(eventId, realm)
|
||||
@ -107,7 +111,7 @@ internal class TimelineEventDecryptor(
|
||||
eventEntity.setDecryptionResult(result)
|
||||
}
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.v("Failed to decrypt event $eventId $e")
|
||||
Timber.v(e, "Failed to decrypt event $eventId")
|
||||
if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||
// Keep track of unknown sessions to automatically try to decrypt on new session
|
||||
realm.executeTransaction {
|
||||
@ -116,10 +120,7 @@ internal class TimelineEventDecryptor(
|
||||
event.content?.toModel<EncryptedEventContent>()?.let { content ->
|
||||
content.sessionId?.let { sessionId ->
|
||||
synchronized(unknownSessionsFailure) {
|
||||
val list = unknownSessionsFailure[sessionId]
|
||||
?: ArrayList<String>().also {
|
||||
unknownSessionsFailure[sessionId] = it
|
||||
}
|
||||
val list = unknownSessionsFailure.getOrPut(sessionId) { ArrayList() }
|
||||
list.add(eventId)
|
||||
}
|
||||
}
|
||||
|
@ -461,9 +461,9 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
|
||||
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
|
||||
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val cipherOutputStream = CipherOutputStream(outputStream, inputCipher)
|
||||
cipherOutputStream.write(secret)
|
||||
cipherOutputStream.close()
|
||||
CipherOutputStream(outputStream, inputCipher).use {
|
||||
it.write(secret)
|
||||
}
|
||||
|
||||
return outputStream.toByteArray()
|
||||
}
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.session.sync
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
@ -41,9 +40,9 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
|
||||
initialSyncProgressService?.reportProgress(((index / total.toFloat()) * 100).toInt())
|
||||
// Decrypt event if necessary
|
||||
decryptEvent(event, null)
|
||||
if (TextUtils.equals(event.getClearType(), EventType.MESSAGE)
|
||||
if (event.getClearType() == EventType.MESSAGE
|
||||
&& event.getClearContent()?.toModel<MessageContent>()?.type == "m.bad.encrypted") {
|
||||
Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : " + event.content)
|
||||
Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}")
|
||||
} else {
|
||||
sasVerificationService.onToDeviceEvent(event)
|
||||
cryptoService.onToDeviceEvent(event)
|
||||
|
@ -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
|
||||
|
@ -31,7 +31,8 @@ import im.vector.matrix.android.internal.task.TaskThread
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import timber.log.Timber
|
||||
import java.net.SocketTimeoutException
|
||||
import java.util.*
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
|
||||
/**
|
||||
* Can execute periodic sync task.
|
||||
|
@ -31,18 +31,10 @@ internal class DirectChatsHelper @Inject constructor(@SessionDatabase
|
||||
*/
|
||||
fun getLocalUserAccount(filterRoomId: String? = null): MutableMap<String, MutableList<String>> {
|
||||
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||
val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
|
||||
val directChatsMap = mutableMapOf<String, MutableList<String>>()
|
||||
for (directRoom in currentDirectRooms) {
|
||||
if (directRoom.roomId == filterRoomId) continue
|
||||
val directUserId = directRoom.directUserId ?: continue
|
||||
directChatsMap
|
||||
.getOrPut(directUserId, { arrayListOf() })
|
||||
.apply {
|
||||
add(directRoom.roomId)
|
||||
}
|
||||
}
|
||||
directChatsMap
|
||||
RoomSummaryEntity.getDirectRooms(realm)
|
||||
.asSequence()
|
||||
.filter { it.roomId != filterRoomId && it.directUserId != null }
|
||||
.groupByTo(mutableMapOf(), { it.directUserId!! }, { it.roomId })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.task
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
internal fun <PARAMS, RESULT> Task<PARAMS, RESULT>.configureWith(params: PARAMS,
|
||||
init: (ConfigurableTask.Builder<PARAMS, RESULT>.() -> Unit) = {}
|
||||
|
@ -59,19 +59,11 @@ object CompatUtil {
|
||||
private const val SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED = "android_version_when_key_has_been_generated"
|
||||
|
||||
private var sSecretKeyAndVersion: SecretKeyAndVersion? = null
|
||||
private var sPrng: SecureRandom? = null
|
||||
|
||||
/**
|
||||
* Returns the unique SecureRandom instance shared for all local storage encryption operations.
|
||||
*/
|
||||
private val prng: SecureRandom
|
||||
get() {
|
||||
if (sPrng == null) {
|
||||
sPrng = SecureRandom()
|
||||
}
|
||||
|
||||
return sPrng!!
|
||||
}
|
||||
private val prng: SecureRandom by lazy(LazyThreadSafetyMode.NONE) { SecureRandom() }
|
||||
|
||||
/**
|
||||
* Create a GZIPOutputStream instance
|
||||
|
@ -24,12 +24,9 @@ import java.security.MessageDigest
|
||||
fun String.md5() = try {
|
||||
val digest = MessageDigest.getInstance("md5")
|
||||
digest.update(toByteArray())
|
||||
val bytes = digest.digest()
|
||||
val sb = StringBuilder()
|
||||
for (i in bytes.indices) {
|
||||
sb.append(String.format("%02X", bytes[i]))
|
||||
}
|
||||
sb.toString().toLowerCase()
|
||||
digest.digest()
|
||||
.joinToString("") { String.format("%02X", it) }
|
||||
.toLowerCase()
|
||||
} catch (exc: Exception) {
|
||||
// Should not happen, but just in case
|
||||
hashCode().toString()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ package im.vector.riotx.gplay.push.fcm
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.TextUtils
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import com.google.firebase.messaging.FirebaseMessagingService
|
||||
@ -214,10 +213,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||
}
|
||||
} else {
|
||||
if (notifiableEvent is NotifiableMessageEvent) {
|
||||
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
|
||||
if (notifiableEvent.senderName.isNullOrEmpty()) {
|
||||
notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
|
||||
}
|
||||
if (TextUtils.isEmpty(notifiableEvent.roomName)) {
|
||||
if (notifiableEvent.roomName.isNullOrEmpty()) {
|
||||
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -104,12 +106,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)
|
||||
@ -136,8 +136,6 @@ interface ScreenComponent {
|
||||
|
||||
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
|
||||
|
||||
fun inject(quickReactionFragment: QuickReactionFragment)
|
||||
|
||||
fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity)
|
||||
|
||||
fun inject(loginActivity: LoginActivity)
|
||||
|
@ -18,7 +18,6 @@ package im.vector.riotx.core.dialogs
|
||||
|
||||
import android.app.Activity
|
||||
import android.text.Editable
|
||||
import android.text.TextUtils
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
@ -45,15 +44,15 @@ class ExportKeysDialog {
|
||||
val textWatcher = object : SimpleTextWatcher() {
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
when {
|
||||
TextUtils.isEmpty(passPhrase1EditText.text) -> {
|
||||
passPhrase1EditText.text.isNullOrEmpty() -> {
|
||||
exportButton.isEnabled = false
|
||||
passPhrase2Til.error = null
|
||||
}
|
||||
TextUtils.equals(passPhrase1EditText.text, passPhrase2EditText.text) -> {
|
||||
passPhrase1EditText.text == passPhrase2EditText.text -> {
|
||||
exportButton.isEnabled = true
|
||||
passPhrase2Til.error = null
|
||||
}
|
||||
else -> {
|
||||
else -> {
|
||||
exportButton.isEnabled = false
|
||||
passPhrase2Til.error = activity.getString(R.string.passphrase_passphrase_does_not_match)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -21,9 +21,7 @@ import android.content.ClipDescription
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import androidx.core.util.PatternsCompat.WEB_URL
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Inspired from Riot code: RoomMediaMessage.java
|
||||
@ -69,34 +67,28 @@ fun analyseIntent(intent: Intent): List<ExternalIntentData> {
|
||||
|
||||
// chrome adds many items when sharing an web page link
|
||||
// so, test first the type
|
||||
if (TextUtils.equals(intent.type, ClipDescription.MIMETYPE_TEXT_PLAIN)) {
|
||||
if (intent.type == ClipDescription.MIMETYPE_TEXT_PLAIN) {
|
||||
var message: String? = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
|
||||
if (null == message) {
|
||||
val sequence = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
|
||||
if (null != sequence) {
|
||||
message = sequence.toString()
|
||||
}
|
||||
}
|
||||
?: intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
|
||||
|
||||
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
||||
|
||||
if (!TextUtils.isEmpty(subject)) {
|
||||
if (TextUtils.isEmpty(message)) {
|
||||
if (!subject.isNullOrEmpty()) {
|
||||
if (message.isNullOrEmpty()) {
|
||||
message = subject
|
||||
} else if (WEB_URL.matcher(message!!).matches()) {
|
||||
} else if (WEB_URL.matcher(message).matches()) {
|
||||
message = subject + "\n" + message
|
||||
}
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(message)) {
|
||||
externalIntentDataList.add(ExternalIntentData.IntentDataText(message!!, null, intent.type))
|
||||
if (!message.isNullOrEmpty()) {
|
||||
externalIntentDataList.add(ExternalIntentData.IntentDataText(message, null, intent.type))
|
||||
return externalIntentDataList
|
||||
}
|
||||
}
|
||||
|
||||
var clipData: ClipData? = null
|
||||
var mimetypes: MutableList<String>? = null
|
||||
var mimeTypes: List<String>? = null
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
clipData = intent.clipData
|
||||
@ -106,41 +98,26 @@ fun analyseIntent(intent: Intent): List<ExternalIntentData> {
|
||||
if (null != clipData) {
|
||||
if (null != clipData.description) {
|
||||
if (0 != clipData.description.mimeTypeCount) {
|
||||
mimetypes = ArrayList()
|
||||
|
||||
for (i in 0 until clipData.description.mimeTypeCount) {
|
||||
mimetypes.add(clipData.description.getMimeType(i))
|
||||
mimeTypes = with(clipData.description) {
|
||||
List(mimeTypeCount) { getMimeType(it) }
|
||||
}
|
||||
|
||||
// if the filter is "accept anything" the mimetype does not make sense
|
||||
if (1 == mimetypes.size) {
|
||||
if (mimetypes[0].endsWith("/*")) {
|
||||
mimetypes = null
|
||||
if (1 == mimeTypes.size) {
|
||||
if (mimeTypes[0].endsWith("/*")) {
|
||||
mimeTypes = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val count = clipData.itemCount
|
||||
|
||||
for (i in 0 until count) {
|
||||
for (i in 0 until clipData.itemCount) {
|
||||
val item = clipData.getItemAt(i)
|
||||
var mimetype: String? = null
|
||||
val mimeType = mimeTypes?.getOrElse(i) { mimeTypes[0] }
|
||||
// uris list is not a valid mimetype
|
||||
.takeUnless { it == ClipDescription.MIMETYPE_TEXT_URILIST }
|
||||
|
||||
if (null != mimetypes) {
|
||||
if (i < mimetypes.size) {
|
||||
mimetype = mimetypes[i]
|
||||
} else {
|
||||
mimetype = mimetypes[0]
|
||||
}
|
||||
|
||||
// uris list is not a valid mimetype
|
||||
if (TextUtils.equals(mimetype, ClipDescription.MIMETYPE_TEXT_URILIST)) {
|
||||
mimetype = null
|
||||
}
|
||||
}
|
||||
|
||||
externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimetype))
|
||||
externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimeType))
|
||||
}
|
||||
} else if (null != intent.data) {
|
||||
externalIntentDataList.add(ExternalIntentData.IntentDataUri(intent.data!!))
|
||||
|
@ -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) } }
|
||||
}
|
@ -17,7 +17,6 @@
|
||||
package im.vector.riotx.core.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.RadioGroup
|
||||
@ -84,7 +83,7 @@ class BingRulePreference : VectorPreference {
|
||||
val ruleStatusIndex: Int
|
||||
get() {
|
||||
if (null != rule) {
|
||||
if (TextUtils.equals(rule!!.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
|
||||
if (rule!!.ruleId == BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) {
|
||||
if (rule!!.shouldNotNotify()) {
|
||||
return if (rule!!.isEnabled) {
|
||||
NOTIFICATION_OFF_INDEX
|
||||
@ -143,7 +142,7 @@ class BingRulePreference : VectorPreference {
|
||||
if (null != this.rule && index != ruleStatusIndex) {
|
||||
rule = BingRule(this.rule!!)
|
||||
|
||||
if (TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
|
||||
if (rule.ruleId == BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) {
|
||||
when (index) {
|
||||
NOTIFICATION_OFF_INDEX -> {
|
||||
rule.isEnabled = true
|
||||
@ -164,8 +163,8 @@ class BingRulePreference : VectorPreference {
|
||||
}
|
||||
|
||||
if (NOTIFICATION_OFF_INDEX == index) {
|
||||
if (TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE)
|
||||
|| TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
|
||||
if (this.rule!!.kind == BingRule.KIND_UNDERRIDE
|
||||
|| rule.ruleId == BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) {
|
||||
rule.setNotify(false)
|
||||
} else {
|
||||
rule.isEnabled = false
|
||||
@ -173,11 +172,11 @@ class BingRulePreference : VectorPreference {
|
||||
} else {
|
||||
rule.isEnabled = true
|
||||
rule.setNotify(true)
|
||||
rule.setHighlight(!TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE)
|
||||
&& !TextUtils.equals(rule.ruleId, BingRule.RULE_ID_INVITE_ME)
|
||||
rule.setHighlight(this.rule!!.kind != BingRule.KIND_UNDERRIDE
|
||||
&& rule.ruleId != BingRule.RULE_ID_INVITE_ME
|
||||
&& NOTIFICATION_NOISY_INDEX == index)
|
||||
if (NOTIFICATION_NOISY_INDEX == index) {
|
||||
rule.notificationSound = if (TextUtils.equals(rule.ruleId, BingRule.RULE_ID_CALL)) {
|
||||
rule.notificationSound = if (rule.ruleId == BingRule.RULE_ID_CALL) {
|
||||
BingRule.ACTION_VALUE_RING
|
||||
} else {
|
||||
BingRule.ACTION_VALUE_DEFAULT
|
||||
|
@ -18,7 +18,6 @@ package im.vector.riotx.core.resources
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import android.webkit.MimeTypeMap
|
||||
import im.vector.riotx.core.utils.getFileExtension
|
||||
import timber.log.Timber
|
||||
@ -73,7 +72,7 @@ fun openResource(context: Context, uri: Uri, providedMimetype: String?): Resourc
|
||||
var mimetype = providedMimetype
|
||||
try {
|
||||
// if the mime type is not provided, try to find it out
|
||||
if (TextUtils.isEmpty(mimetype)) {
|
||||
if (mimetype.isNullOrEmpty()) {
|
||||
mimetype = context.contentResolver.getType(uri)
|
||||
|
||||
// try to find the mimetype from the filename
|
||||
|
@ -20,7 +20,6 @@ import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.text.SpannableString
|
||||
import android.text.TextPaint
|
||||
import android.text.TextUtils
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.util.AttributeSet
|
||||
@ -168,7 +167,7 @@ class NotificationAreaView @JvmOverloads constructor(
|
||||
} else {
|
||||
imageView.setImageResource(R.drawable.scrolldown)
|
||||
messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
|
||||
if (!TextUtils.isEmpty(state.message)) {
|
||||
if (!state.message.isNullOrEmpty()) {
|
||||
messageView.text = SpannableString(state.message)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -17,7 +17,6 @@
|
||||
package im.vector.riotx.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
@ -60,7 +59,7 @@ private fun logAction(file: File): Boolean {
|
||||
if (file.isDirectory) {
|
||||
Timber.v(file.toString())
|
||||
} else {
|
||||
Timber.v(file.toString() + " " + file.length() + " bytes")
|
||||
Timber.v("$file ${file.length()} bytes")
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -96,26 +95,19 @@ private fun recursiveActionOnFile(file: File, action: ActionOnFile): Boolean {
|
||||
fun getFileExtension(fileUri: String): String? {
|
||||
var reducedStr = fileUri
|
||||
|
||||
if (!TextUtils.isEmpty(reducedStr)) {
|
||||
if (reducedStr.isNotEmpty()) {
|
||||
// Remove fragment
|
||||
val fragment = fileUri.lastIndexOf('#')
|
||||
if (fragment > 0) {
|
||||
reducedStr = fileUri.substring(0, fragment)
|
||||
}
|
||||
reducedStr = reducedStr.substringBeforeLast('#')
|
||||
|
||||
// Remove query
|
||||
val query = reducedStr.lastIndexOf('?')
|
||||
if (query > 0) {
|
||||
reducedStr = reducedStr.substring(0, query)
|
||||
}
|
||||
reducedStr = reducedStr.substringBeforeLast('?')
|
||||
|
||||
// Remove path
|
||||
val filenamePos = reducedStr.lastIndexOf('/')
|
||||
val filename = if (0 <= filenamePos) reducedStr.substring(filenamePos + 1) else reducedStr
|
||||
val filename = reducedStr.substringAfterLast('/')
|
||||
|
||||
// Contrary to method MimeTypeMap.getFileExtensionFromUrl, we do not check the pattern
|
||||
// See https://stackoverflow.com/questions/14320527/android-should-i-use-mimetypemap-getfileextensionfromurl-bugs
|
||||
if (!filename.isEmpty()) {
|
||||
if (filename.isNotEmpty()) {
|
||||
val dotPos = filename.lastIndexOf('.')
|
||||
if (0 <= dotPos) {
|
||||
val ext = filename.substring(dotPos + 1)
|
||||
@ -134,15 +126,11 @@ fun getFileExtension(fileUri: String): String? {
|
||||
* Size
|
||||
* ========================================================================================== */
|
||||
|
||||
fun getSizeOfFiles(context: Context, root: File): Int {
|
||||
Timber.v("Get size of " + root.absolutePath)
|
||||
return if (root.isDirectory) {
|
||||
root.list()
|
||||
.map {
|
||||
getSizeOfFiles(context, File(root, it))
|
||||
}
|
||||
.fold(0, { acc, other -> acc + other })
|
||||
} else {
|
||||
root.length().toInt()
|
||||
}
|
||||
fun getSizeOfFiles(root: File): Int {
|
||||
return root.walkTopDown()
|
||||
.onEnter {
|
||||
Timber.v("Get size of ${it.absolutePath}")
|
||||
true
|
||||
}
|
||||
.sumBy { it.length().toInt() }
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.riotx.R
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
private const val LOG_TAG = "PermissionUtils"
|
||||
|
||||
@ -74,7 +73,7 @@ const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
|
||||
*/
|
||||
fun logPermissionStatuses(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val permissions = Arrays.asList(
|
||||
val permissions = listOf(
|
||||
Manifest.permission.CAMERA,
|
||||
Manifest.permission.RECORD_AUDIO,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
@ -213,7 +212,7 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
||||
.setMessage(rationaleMessage)
|
||||
.setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() }
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
if (!permissionsListToBeGranted.isEmpty()) {
|
||||
if (permissionsListToBeGranted.isNotEmpty()) {
|
||||
fragment?.requestPermissions(permissionsListToBeGranted.toTypedArray(), requestCode)
|
||||
?: run {
|
||||
ActivityCompat.requestPermissions(activity, permissionsListToBeGranted.toTypedArray(), requestCode)
|
||||
|
@ -24,9 +24,9 @@ import java.util.*
|
||||
object TextUtils {
|
||||
|
||||
private val suffixes = TreeMap<Int, String>().also {
|
||||
it.put(1000, "k")
|
||||
it.put(1000000, "M")
|
||||
it.put(1000000000, "G")
|
||||
it[1000] = "k"
|
||||
it[1000000] = "M"
|
||||
it[1000000000] = "G"
|
||||
}
|
||||
|
||||
fun formatCountToShortDecimal(value: Int): String {
|
||||
|
@ -17,7 +17,6 @@ package im.vector.riotx.features.crypto.keysbackup.setup
|
||||
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
@ -122,7 +121,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
|
||||
})
|
||||
|
||||
viewModel.passphrase.observe(this, Observer<String> { newValue ->
|
||||
if (TextUtils.isEmpty(newValue)) {
|
||||
if (newValue.isEmpty()) {
|
||||
viewModel.passwordStrength.value = null
|
||||
} else {
|
||||
AsyncTask.execute {
|
||||
@ -172,7 +171,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
|
||||
@OnClick(R.id.keys_backup_setup_step2_button)
|
||||
fun doNext() {
|
||||
when {
|
||||
TextUtils.isEmpty(viewModel.passphrase.value) -> {
|
||||
viewModel.passphrase.value.isNullOrEmpty() -> {
|
||||
viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message)
|
||||
}
|
||||
viewModel.passphrase.value != viewModel.confirmPassphrase.value -> {
|
||||
@ -192,7 +191,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
|
||||
@OnClick(R.id.keys_backup_setup_step2_skip_button)
|
||||
fun skipPassphrase() {
|
||||
when {
|
||||
TextUtils.isEmpty(viewModel.passphrase.value) -> {
|
||||
viewModel.passphrase.value.isNullOrEmpty() -> {
|
||||
// Generate a recovery key for the user
|
||||
viewModel.megolmBackupCreationInfo = null
|
||||
|
||||
|
@ -20,7 +20,6 @@
|
||||
package im.vector.riotx.features.crypto.keysrequest
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
||||
@ -39,7 +38,8 @@ import im.vector.riotx.features.popup.PopupAlertManager
|
||||
import timber.log.Timber
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.collections.ArrayList
|
||||
@ -100,7 +100,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||
alertsToRequests[mappingKey] = ArrayList<IncomingRoomKeyRequest>().apply { this.add(request) }
|
||||
|
||||
// Add a notification for every incoming request
|
||||
session?.downloadKeys(Arrays.asList(userId), false, object : MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
session?.downloadKeys(listOf(userId), false, object : MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
override fun onSuccess(data: MXUsersDevicesMap<MXDeviceInfo>) {
|
||||
val deviceInfo = data.getObject(userId, deviceId)
|
||||
|
||||
@ -147,7 +147,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||
wasNewDevice: Boolean,
|
||||
deviceInfo: MXDeviceInfo?,
|
||||
moreInfo: DeviceInfo? = null) {
|
||||
val deviceName = if (TextUtils.isEmpty(deviceInfo!!.displayName())) deviceInfo.deviceId else deviceInfo.displayName()
|
||||
val deviceName = if (deviceInfo!!.displayName().isNullOrEmpty()) deviceInfo.deviceId else deviceInfo.displayName()
|
||||
val dialogText: String?
|
||||
|
||||
if (moreInfo != null) {
|
||||
@ -244,12 +244,12 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||
val deviceId = request.deviceId
|
||||
val requestId = request.requestId
|
||||
|
||||
if (TextUtils.isEmpty(userId) || TextUtils.isEmpty(deviceId) || TextUtils.isEmpty(requestId)) {
|
||||
if (userId.isNullOrEmpty() || deviceId.isNullOrEmpty() || requestId.isNullOrEmpty()) {
|
||||
Timber.e("## handleKeyRequestCancellation() : invalid parameters")
|
||||
return
|
||||
}
|
||||
|
||||
val alertMgrUniqueKey = alertManagerId(deviceId!!, userId!!)
|
||||
val alertMgrUniqueKey = alertManagerId(deviceId, userId)
|
||||
alertsToRequests[alertMgrUniqueKey]?.removeAll {
|
||||
it.deviceId == request.deviceId
|
||||
&& it.userId == request.userId
|
||||
|
@ -179,7 +179,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.otaliastudios.autocomplete.Autocomplete
|
||||
import com.otaliastudios.autocomplete.AutocompleteCallback
|
||||
import com.otaliastudios.autocomplete.CharPolicy
|
||||
@ -66,6 +68,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
|
||||
@ -96,8 +99,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
|
||||
@ -274,6 +281,10 @@ class RoomDetailFragment :
|
||||
syncStateView.render(syncState)
|
||||
}
|
||||
|
||||
roomDetailViewModel.requestLiveData.observeEvent(this) {
|
||||
displayRoomDetailActionResult(it)
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
when (val sharedData = roomDetailArgs.sharedData) {
|
||||
is SharedData.Text -> roomDetailViewModel.process(RoomDetailActions.SendMessage(sharedData.text, false))
|
||||
@ -281,6 +292,7 @@ class RoomDetailFragment :
|
||||
null -> Timber.v("No share data to process")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@ -438,7 +450,7 @@ class RoomDetailFragment :
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -732,17 +744,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) {
|
||||
@ -760,6 +836,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 {
|
||||
@ -872,7 +956,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))
|
||||
@ -880,7 +964,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")
|
||||
}
|
||||
|
||||
@ -929,23 +1013,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
|
||||
@ -973,10 +1057,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
|
||||
@ -987,7 +1071,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
|
||||
@ -998,31 +1082,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()
|
||||
}
|
||||
}
|
||||
|
@ -16,14 +16,10 @@
|
||||
|
||||
package im.vector.riotx.features.home.room.detail
|
||||
|
||||
import android.text.TextUtils
|
||||
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
|
||||
@ -92,6 +88,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
|
||||
|
||||
@ -150,6 +151,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -371,7 +373,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
val document = parser.parse(finalText)
|
||||
val renderer = HtmlRenderer.builder().build()
|
||||
val htmlText = renderer.render(document)
|
||||
if (TextUtils.equals(finalText, htmlText)) {
|
||||
if (finalText == htmlText) {
|
||||
room.sendTextMessage(finalText)
|
||||
} else {
|
||||
room.sendFormattedTextMessage(finalText, htmlText)
|
||||
@ -396,19 +398,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
|
||||
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
|
||||
val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
|
||||
val quotedTextMsg = StringBuilder()
|
||||
if (messageParagraphs != null) {
|
||||
for (i in messageParagraphs.indices) {
|
||||
if (messageParagraphs[i].trim() != "") {
|
||||
quotedTextMsg.append("> ").append(messageParagraphs[i])
|
||||
}
|
||||
return buildString {
|
||||
if (messageParagraphs != null) {
|
||||
for (i in messageParagraphs.indices) {
|
||||
if (messageParagraphs[i].isNotBlank()) {
|
||||
append("> ")
|
||||
append(messageParagraphs[i])
|
||||
}
|
||||
|
||||
if (i + 1 != messageParagraphs.size) {
|
||||
quotedTextMsg.append("\n\n")
|
||||
if (i != messageParagraphs.lastIndex) {
|
||||
append("\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
append("\n\n")
|
||||
append(myText)
|
||||
}
|
||||
return "$quotedTextMsg\n\n$myText"
|
||||
}
|
||||
|
||||
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
|
||||
@ -440,7 +445,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) {
|
||||
@ -449,14 +454,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -680,6 +685,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 {
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user