mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-24 23:09:02 +03:00
Merge branch 'develop' into feature/attachments
This commit is contained in:
commit
2974f8b200
138 changed files with 2236 additions and 2113 deletions
CHANGES.mdCONTRIBUTING.mdgradle.properties
matrix-sdk-android
build.gradle
src
main/java/im/vector/matrix/android
api
internal
crypto
DefaultCryptoService.ktDeviceListManager.ktIncomingRoomKeyRequestManager.ktMXMegolmExportEncryption.ktMXOlmDevice.ktMyDeviceInfoHolder.ktOneTimeKeysUploader.ktRoomDecryptorProvider.kt
actions
algorithms
keysbackup
model
store/db
tasks
database
network
session
content
group
notification
room
DefaultRoom.ktDefaultRoomService.ktRoomAPI.ktRoomFactory.ktRoomModule.kt
membership
read
relation
reporting
timeline
securestorage
sync
user/accountdata
task
util
test/java/im/vector/matrix/android/api/pushrules
vector
build.gradle
src
debug/java/im/vector/riotx/features/debug
gplay/java/im/vector/riotx/gplay/push/fcm
main/java/im/vector/riotx
core
di
dialogs
intent
platform
preference
resources
ui/views
utils
features
crypto
home
HomeActivity.kt
room/detail
RoomDetailActions.ktRoomDetailFragment.ktRoomDetailViewModel.kt
readreceipts
timeline
action
BottomSheetItemAction.ktBottomSheetItemMessagePreview.ktBottomSheetItemQuickReactions.ktBottomSheetItemSendState.ktBottomSheetItemSeparator.ktMessageActionsBottomSheet.ktMessageActionsEpoxyController.ktMessageActionsViewModel.ktMessageMenuFragment.ktMessageMenuViewModel.ktQuickReactionFragment.ktQuickReactionViewModel.ktSimpleAction.kt
edithistory
|
@ -10,6 +10,8 @@ Improvements:
|
||||||
- Handle read markers (#84)
|
- Handle read markers (#84)
|
||||||
- Attachments: start using system pickers (#52)
|
- Attachments: start using system pickers (#52)
|
||||||
- Attachments: start handling incoming share (#58)
|
- Attachments: start handling incoming share (#58)
|
||||||
|
- Mark all messages as read (#396)
|
||||||
|
- Add ability to report content (#515)
|
||||||
|
|
||||||
Other changes:
|
Other changes:
|
||||||
- Accessibility improvements to read receipts in the room timeline and reactions emoji chooser
|
- 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)
|
- 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)
|
- 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)
|
- Invitation notifications are not dismissed automatically if room is joined from another client (#347)
|
||||||
|
- Opening links from RiotX reuses browser tab (#599)
|
||||||
|
|
||||||
Translations:
|
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:
|
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
|
<pre>
|
||||||
> ./ktlint --android -v
|
./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
|
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
|
### Unit tests
|
||||||
|
|
||||||
Make sure the following commands execute without any error:
|
Make sure the following commands execute without any error:
|
||||||
|
|
||||||
> ./gradlew testGplayReleaseUnitTest
|
<pre>
|
||||||
|
./gradlew testGplayReleaseUnitTest
|
||||||
|
</pre>
|
||||||
|
|
||||||
### Tests
|
### 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.
|
Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient.
|
||||||
|
|
||||||
### Internationalisation
|
### Internationalisation
|
||||||
|
|
|
@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m
|
||||||
|
|
||||||
|
|
||||||
vector.debugPrivateData=false
|
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
|
# 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
|
#vector.debugPrivateData=true
|
||||||
|
|
|
@ -102,7 +102,7 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
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"
|
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||||
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
|
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
|
||||||
|
@ -110,8 +110,8 @@ dependencies {
|
||||||
// Network
|
// Network
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
|
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
|
||||||
implementation 'com.squareup.retrofit2:converter-moshi:2.6.2'
|
implementation 'com.squareup.retrofit2:converter-moshi:2.6.2'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.2.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
|
||||||
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.0'
|
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
|
||||||
implementation 'com.novoda:merlin:1.2.0'
|
implementation 'com.novoda:merlin:1.2.0'
|
||||||
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
||||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$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.api.comparators.DatedObjectComparators
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import java.util.Collections
|
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* MXDeviceInfo
|
* MXDeviceInfo
|
||||||
|
@ -29,6 +28,6 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
|
||||||
?.chunked(4)
|
?.chunked(4)
|
||||||
?.joinToString(separator = " ")
|
?.joinToString(separator = " ")
|
||||||
|
|
||||||
fun List<DeviceInfo>.sortByLastSeen() {
|
fun MutableList<DeviceInfo>.sortByLastSeen() {
|
||||||
Collections.sort(this, DatedObjectComparators.descComparator)
|
sortWith(DatedObjectComparators.descComparator)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,9 +30,9 @@ object MatrixLinkify {
|
||||||
*
|
*
|
||||||
* @param spannable the text in which the matrix items has to be clickable.
|
* @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
|
// sanity checks
|
||||||
if (spannable.isNullOrEmpty()) {
|
if (spannable.isEmpty()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
val text = spannable.toString()
|
val text = spannable.toString()
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.api.permalinks
|
package im.vector.matrix.android.api.permalinks
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
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
|
* @return the permalink, or null in case of error
|
||||||
*/
|
*/
|
||||||
fun createPermalink(id: String): String? {
|
fun createPermalink(id: String): String? {
|
||||||
return if (TextUtils.isEmpty(id)) {
|
return if (id.isEmpty()) {
|
||||||
null
|
null
|
||||||
} else MATRIX_TO_URL_BASE + escape(id)
|
} 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"
|
* @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
|
* @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink
|
||||||
*/
|
*/
|
||||||
fun getLinkedId(url: String?): String? {
|
fun getLinkedId(url: String): String? {
|
||||||
val isSupported = url != null && url.startsWith(MATRIX_TO_URL_BASE)
|
val isSupported = url.startsWith(MATRIX_TO_URL_BASE)
|
||||||
|
|
||||||
return if (isSupported) {
|
return if (isSupported) {
|
||||||
url!!.substring(MATRIX_TO_URL_BASE.length)
|
url.substring(MATRIX_TO_URL_BASE.length)
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,6 +85,6 @@ object PermalinkFactory {
|
||||||
* @return the escaped id
|
* @return the escaped id
|
||||||
*/
|
*/
|
||||||
private fun escape(id: String): String {
|
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
|
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.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
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.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
|
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 {
|
fun isSatisfied(event: Event, displayName: String): Boolean {
|
||||||
var message = when (event.type) {
|
val message = when (event.type) {
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE -> {
|
||||||
event.content.toModel<MessageContent>()
|
event.content.toModel<MessageContent>()
|
||||||
}
|
}
|
||||||
|
@ -59,20 +57,18 @@ class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
|
||||||
*/
|
*/
|
||||||
fun caseInsensitiveFind(subString: String, longString: String): Boolean {
|
fun caseInsensitiveFind(subString: String, longString: String): Boolean {
|
||||||
// add sanity checks
|
// add sanity checks
|
||||||
if (TextUtils.isEmpty(subString) || TextUtils.isEmpty(longString)) {
|
if (subString.isEmpty() || longString.isEmpty()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var res = false
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val pattern = Pattern.compile("(\\W|^)" + Pattern.quote(subString) + "(\\W|$)", Pattern.CASE_INSENSITIVE)
|
val regex = Regex("(\\W|^)" + Regex.escape(subString) + "(\\W|$)", RegexOption.IGNORE_CASE)
|
||||||
res = pattern.matcher(longString).find()
|
return regex.containsMatchIn(longString)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "## caseInsensitiveFind() : failed")
|
Timber.e(e, "## caseInsensitiveFind() : failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.events.model
|
package im.vector.matrix.android.api.session.events.model
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||||
|
@ -35,11 +34,10 @@ typealias Content = JsonDict
|
||||||
* This methods is a facility method to map a json content to a model.
|
* This methods is a facility method to map a json content to a model.
|
||||||
*/
|
*/
|
||||||
inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
|
inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
|
||||||
return this?.let {
|
|
||||||
val moshi = MoshiProvider.providesMoshi()
|
val moshi = MoshiProvider.providesMoshi()
|
||||||
val moshiAdapter = moshi.adapter(T::class.java)
|
val moshiAdapter = moshi.adapter(T::class.java)
|
||||||
return try {
|
return try {
|
||||||
moshiAdapter.fromJsonValue(it)
|
moshiAdapter.fromJsonValue(this)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (catchError) {
|
if (catchError) {
|
||||||
Timber.e(e, "To model failed : $e")
|
Timber.e(e, "To model failed : $e")
|
||||||
|
@ -49,18 +47,15 @@ inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This methods is a facility method to map a model to a json Content
|
* This methods is a facility method to map a model to a json Content
|
||||||
*/
|
*/
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
inline fun <reified T> T?.toContent(): Content? {
|
inline fun <reified T> T.toContent(): Content {
|
||||||
return this?.let {
|
|
||||||
val moshi = MoshiProvider.providesMoshi()
|
val moshi = MoshiProvider.providesMoshi()
|
||||||
val moshiAdapter = moshi.adapter(T::class.java)
|
val moshiAdapter = moshi.adapter(T::class.java)
|
||||||
return moshiAdapter.toJsonValue(it) as Content
|
return moshiAdapter.toJsonValue(this) as Content
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -106,7 +101,7 @@ data class Event(
|
||||||
* @return true if this event is encrypted.
|
* @return true if this event is encrypted.
|
||||||
*/
|
*/
|
||||||
fun isEncrypted(): Boolean {
|
fun isEncrypted(): Boolean {
|
||||||
return TextUtils.equals(type, EventType.ENCRYPTED)
|
return type == EventType.ENCRYPTED
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,7 +134,7 @@ data class Event(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toContentStringWithIndent(): String {
|
fun toContentStringWithIndent(): String {
|
||||||
val contentMap = toContent()?.toMutableMap() ?: HashMap()
|
val contentMap = toContent().toMutableMap()
|
||||||
return JSONObject(contentMap).toString(4)
|
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.members.MembershipService
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
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.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.read.ReadService
|
||||||
import im.vector.matrix.android.api.session.room.send.DraftService
|
import im.vector.matrix.android.api.session.room.send.DraftService
|
||||||
import im.vector.matrix.android.api.session.room.send.SendService
|
import im.vector.matrix.android.api.session.room.send.SendService
|
||||||
|
@ -38,6 +39,7 @@ interface Room :
|
||||||
ReadService,
|
ReadService,
|
||||||
MembershipService,
|
MembershipService,
|
||||||
StateService,
|
StateService,
|
||||||
|
ReportingService,
|
||||||
RelationService,
|
RelationService,
|
||||||
RoomCryptoService {
|
RoomCryptoService {
|
||||||
|
|
||||||
|
|
|
@ -53,4 +53,9 @@ interface RoomService {
|
||||||
* @return the [LiveData] of [RoomSummary]
|
* @return the [LiveData] of [RoomSummary]
|
||||||
*/
|
*/
|
||||||
fun liveRoomSummaries(): LiveData<List<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
|
package im.vector.matrix.android.api.session.room.model
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
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.
|
* Class representing the EventType.EVENT_TYPE_STATE_ROOM_POWER_LEVELS state event content.
|
||||||
|
@ -46,13 +44,7 @@ data class PowerLevels(
|
||||||
* @return the power level
|
* @return the power level
|
||||||
*/
|
*/
|
||||||
fun getUserPowerLevel(userId: String): Int {
|
fun getUserPowerLevel(userId: String): Int {
|
||||||
// sanity check
|
return users.getOrElse(userId) { usersDefault }
|
||||||
if (!TextUtils.isEmpty(userId)) {
|
|
||||||
val powerLevel = users[userId]
|
|
||||||
return powerLevel ?: usersDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
return usersDefault
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,10 +53,8 @@ data class PowerLevels(
|
||||||
* @param userId the user
|
* @param userId the user
|
||||||
* @param powerLevel the new power level
|
* @param powerLevel the new power level
|
||||||
*/
|
*/
|
||||||
fun setUserPowerLevel(userId: String?, powerLevel: Int) {
|
fun setUserPowerLevel(userId: String, powerLevel: Int) {
|
||||||
if (null != userId) {
|
users[userId] = powerLevel
|
||||||
users[userId] = Integer.valueOf(powerLevel)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,7 +65,7 @@ data class PowerLevels(
|
||||||
* @return true if the user can send the event
|
* @return true if the user can send the event
|
||||||
*/
|
*/
|
||||||
fun maySendEventOfType(eventTypeString: String, userId: String): Boolean {
|
fun maySendEventOfType(eventTypeString: String, userId: String): Boolean {
|
||||||
return if (!TextUtils.isEmpty(eventTypeString) && !TextUtils.isEmpty(userId)) {
|
return if (eventTypeString.isNotEmpty() && userId.isNotEmpty()) {
|
||||||
getUserPowerLevel(userId) >= minimumPowerLevelForSendingEventAsMessage(eventTypeString)
|
getUserPowerLevel(userId) >= minimumPowerLevelForSendingEventAsMessage(eventTypeString)
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
|
@ -118,18 +108,14 @@ data class PowerLevels(
|
||||||
* @param key the notification key
|
* @param key the notification key
|
||||||
* @return the level
|
* @return the level
|
||||||
*/
|
*/
|
||||||
fun notificationLevel(key: String?): Int {
|
fun notificationLevel(key: String): Int {
|
||||||
if (null != key && notifications.containsKey(key)) {
|
val valAsVoid = notifications[key] ?: return 50
|
||||||
val valAsVoid = notifications[key]
|
|
||||||
|
|
||||||
// the first implementation was a string value
|
// the first implementation was a string value
|
||||||
return if (valAsVoid is String) {
|
return if (valAsVoid is String) {
|
||||||
Integer.parseInt(valAsVoid)
|
valAsVoid.toInt()
|
||||||
} else {
|
} else {
|
||||||
valAsVoid as Int
|
valAsVoid as Int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 50
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,15 +145,7 @@ class CreateRoomParams {
|
||||||
*/
|
*/
|
||||||
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) {
|
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?) {
|
||||||
// Remove the existing value if any.
|
// Remove the existing value if any.
|
||||||
if (initialStates != null && !initialStates!!.isEmpty()) {
|
initialStates?.removeAll { it.getClearType() == EventType.STATE_HISTORY_VISIBILITY }
|
||||||
val newInitialStates = ArrayList<Event>()
|
|
||||||
for (event in initialStates!!) {
|
|
||||||
if (event.getClearType() != EventType.STATE_HISTORY_VISIBILITY) {
|
|
||||||
newInitialStates.add(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
initialStates = newInitialStates
|
|
||||||
}
|
|
||||||
|
|
||||||
if (historyVisibility != null) {
|
if (historyVisibility != null) {
|
||||||
val contentMap = HashMap<String, RoomHistoryVisibility>()
|
val contentMap = HashMap<String, RoomHistoryVisibility>()
|
||||||
|
|
|
@ -50,21 +50,19 @@ interface RelationService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a reaction (emoji) to the targetedEvent.
|
* Sends a reaction (emoji) to the targetedEvent.
|
||||||
* @param reaction the reaction (preferably emoji)
|
|
||||||
* @param targetEventId the id of the event being reacted
|
* @param targetEventId the id of the event being reacted
|
||||||
|
* @param reaction the reaction (preferably emoji)
|
||||||
*/
|
*/
|
||||||
fun sendReaction(reaction: String,
|
fun sendReaction(targetEventId: String,
|
||||||
targetEventId: String): Cancelable
|
reaction: String): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Undo a reaction (emoji) to the targetedEvent.
|
* Undo a reaction (emoji) to the targetedEvent.
|
||||||
* @param reaction the reaction (preferably emoji)
|
|
||||||
* @param targetEventId the id of the event being reacted
|
* @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,
|
fun undoReaction(targetEventId: String,
|
||||||
targetEventId: String,
|
reaction: String): Cancelable
|
||||||
myUserId: String) // : Cancelable
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edit a text message body. Limited to "m.text" contentType
|
* Edit a text message body. Limited to "m.text" contentType
|
||||||
|
@ -92,7 +90,7 @@ interface RelationService {
|
||||||
compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
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>>)
|
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.content.Context
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import arrow.core.Try
|
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
|
@ -359,29 +358,16 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
override fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?) {
|
override fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?) {
|
||||||
// build a devices map
|
// build a devices map
|
||||||
val devicesIdListByUserId = HashMap<String, List<String>>()
|
val devicesIdListByUserId = devices.groupBy({ it.userId }, { it.deviceId })
|
||||||
|
|
||||||
for (di in devices) {
|
for ((userId, deviceIds) in devicesIdListByUserId) {
|
||||||
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) {
|
|
||||||
val storedDeviceIDs = cryptoStore.getUserDevices(userId)
|
val storedDeviceIDs = cryptoStore.getUserDevices(userId)
|
||||||
|
|
||||||
// sanity checks
|
// sanity checks
|
||||||
if (null != storedDeviceIDs) {
|
if (null != storedDeviceIDs) {
|
||||||
var isUpdated = false
|
var isUpdated = false
|
||||||
val deviceIds = devicesIdListByUserId[userId]
|
|
||||||
|
|
||||||
deviceIds?.forEach { deviceId ->
|
deviceIds.forEach { deviceId ->
|
||||||
val device = storedDeviceIDs[deviceId]
|
val device = storedDeviceIDs[deviceId]
|
||||||
|
|
||||||
// assume if the device is either verified or blocked
|
// assume if the device is either verified or blocked
|
||||||
|
@ -549,16 +535,10 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
val t0 = System.currentTimeMillis()
|
val t0 = System.currentTimeMillis()
|
||||||
Timber.v("## encryptEventContent() starts")
|
Timber.v("## encryptEventContent() starts")
|
||||||
runCatching {
|
runCatching {
|
||||||
safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
|
val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
|
||||||
}
|
|
||||||
.fold(
|
|
||||||
{
|
|
||||||
Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
|
Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
|
||||||
callback.onSuccess(MXEncryptEventContentResult(it, EventType.ENCRYPTED))
|
MXEncryptEventContentResult(content, EventType.ENCRYPTED)
|
||||||
},
|
}.foldToCallback(callback)
|
||||||
{ callback.onFailure(it) }
|
|
||||||
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
val algorithm = getEncryptionAlgorithm(roomId)
|
val algorithm = getEncryptionAlgorithm(roomId)
|
||||||
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
|
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
|
||||||
|
@ -776,7 +756,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
GlobalScope.launch(coroutineDispatchers.main) {
|
||||||
runCatching {
|
runCatching {
|
||||||
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
|
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 password the password
|
||||||
* @param anIterationCount the encryption iteration count (0 means no encryption)
|
* @param anIterationCount the encryption iteration count (0 means no encryption)
|
||||||
* @param callback the exported keys
|
|
||||||
*/
|
*/
|
||||||
private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray {
|
private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray {
|
||||||
return withContext(coroutineDispatchers.crypto) {
|
return withContext(coroutineDispatchers.crypto) {
|
||||||
|
@ -813,8 +792,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
progressListener: ProgressListener?,
|
progressListener: ProgressListener?,
|
||||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
GlobalScope.launch(coroutineDispatchers.main) {
|
||||||
|
runCatching {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
withContext(coroutineDispatchers.crypto) {
|
||||||
Try {
|
|
||||||
Timber.v("## importRoomKeys starts")
|
Timber.v("## importRoomKeys starts")
|
||||||
|
|
||||||
val t0 = System.currentTimeMillis()
|
val t0 = System.currentTimeMillis()
|
||||||
|
@ -861,19 +840,14 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
|
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
|
||||||
// force the refresh to ensure that the devices list is up-to-date
|
// force the refresh to ensure that the devices list is up-to-date
|
||||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||||
runCatching { deviceListManager.downloadKeys(userIds, true) }
|
runCatching {
|
||||||
.fold(
|
val keys = deviceListManager.downloadKeys(userIds, true)
|
||||||
{
|
val unknownDevices = getUnknownDevices(keys)
|
||||||
val unknownDevices = getUnknownDevices(it)
|
if (unknownDevices.map.isNotEmpty()) {
|
||||||
if (unknownDevices.map.isEmpty()) {
|
|
||||||
callback.onSuccess(Unit)
|
|
||||||
} else {
|
|
||||||
// trigger an an unknown devices exception
|
// trigger an an unknown devices exception
|
||||||
callback.onFailure(Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)))
|
throw Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices))
|
||||||
}
|
}
|
||||||
},
|
}.foldToCallback(callback)
|
||||||
{ callback.onFailure(it) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import im.vector.matrix.android.api.MatrixPatterns
|
import im.vector.matrix.android.api.MatrixPatterns
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
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.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.SessionScope
|
||||||
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
|
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
// Legacy name: MXDeviceList
|
// Legacy name: MXDeviceList
|
||||||
|
@ -39,13 +37,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
private val downloadKeysForUsersTask: DownloadKeysForUsersTask) {
|
private val downloadKeysForUsersTask: DownloadKeysForUsersTask) {
|
||||||
|
|
||||||
// HS not ready for retry
|
// HS not ready for retry
|
||||||
private val notReadyToRetryHS = HashSet<String>()
|
private val notReadyToRetryHS = mutableSetOf<String>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
var isUpdated = false
|
var isUpdated = false
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||||
for (userId in deviceTrackingStatuses.keys) {
|
for ((userId, status) in deviceTrackingStatuses) {
|
||||||
val status = deviceTrackingStatuses[userId]!!
|
|
||||||
if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) {
|
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.
|
// if a download was in progress when we got shut down, it isn't any more.
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
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 {
|
private fun canRetryKeysDownload(userId: String): Boolean {
|
||||||
var res = false
|
var res = false
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(userId) && userId.contains(":")) {
|
if (':' in userId) {
|
||||||
try {
|
try {
|
||||||
synchronized(notReadyToRetryHS) {
|
synchronized(notReadyToRetryHS) {
|
||||||
res = !notReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1))
|
res = !notReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1))
|
||||||
|
@ -119,11 +116,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
* @param changed the user ids list which have new devices
|
* @param changed the user ids list which have new devices
|
||||||
* @param left the user ids list which left a room
|
* @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
|
var isUpdated = false
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||||
|
|
||||||
if (changed?.isNotEmpty() == true) {
|
|
||||||
for (userId in changed) {
|
for (userId in changed) {
|
||||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
if (deviceTrackingStatuses.containsKey(userId)) {
|
||||||
Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId")
|
Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId")
|
||||||
|
@ -131,9 +127,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
isUpdated = true
|
isUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (left?.isNotEmpty() == true) {
|
|
||||||
for (userId in left) {
|
for (userId in left) {
|
||||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
if (deviceTrackingStatuses.containsKey(userId)) {
|
||||||
Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId")
|
Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId")
|
||||||
|
@ -141,7 +135,6 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
isUpdated = true
|
isUpdated = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (isUpdated) {
|
if (isUpdated) {
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||||
|
@ -153,7 +146,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
* + update
|
* + update
|
||||||
*/
|
*/
|
||||||
fun invalidateAllDeviceLists() {
|
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>) {
|
private fun onKeysDownloadFailed(userIds: List<String>) {
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||||
for (userId in userIds) {
|
userIds.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_PENDING_DOWNLOAD }
|
||||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
|
||||||
}
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,16 +168,11 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
*/
|
*/
|
||||||
private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<MXDeviceInfo> {
|
private fun onKeysDownloadSucceed(userIds: List<String>, failures: Map<String, Map<String, Any>>?): MXUsersDevicesMap<MXDeviceInfo> {
|
||||||
if (failures != null) {
|
if (failures != null) {
|
||||||
val keys = failures.keys
|
for ((k, value) in failures) {
|
||||||
for (k in keys) {
|
val statusCode = when (val status = value["status"]) {
|
||||||
val value = failures[k]
|
is Double -> status.toInt()
|
||||||
if (value!!.containsKey("status")) {
|
is Int -> status.toInt()
|
||||||
val statusCodeAsVoid = value["status"]
|
else -> 0
|
||||||
var statusCode = 0
|
|
||||||
if (statusCodeAsVoid is Double) {
|
|
||||||
statusCode = statusCodeAsVoid.toInt()
|
|
||||||
} else if (statusCodeAsVoid is Int) {
|
|
||||||
statusCode = statusCodeAsVoid.toInt()
|
|
||||||
}
|
}
|
||||||
if (statusCode == 503) {
|
if (statusCode == 503) {
|
||||||
synchronized(notReadyToRetryHS) {
|
synchronized(notReadyToRetryHS) {
|
||||||
|
@ -195,7 +181,6 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||||
val usersDevicesInfoMap = MXUsersDevicesMap<MXDeviceInfo>()
|
val usersDevicesInfoMap = MXUsersDevicesMap<MXDeviceInfo>()
|
||||||
for (userId in userIds) {
|
for (userId in userIds) {
|
||||||
|
@ -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.
|
* Download the device keys for a list of users and stores the keys in the MXStore.
|
||||||
* It must be called in getEncryptingThreadHandler() thread.
|
* It must be called in getEncryptingThreadHandler() thread.
|
||||||
* The callback is called in the UI thread.
|
|
||||||
*
|
*
|
||||||
* @param userIds The users to fetch.
|
* @param userIds The users to fetch.
|
||||||
* @param forceDownload Always download the keys even if cached.
|
* @param forceDownload Always download the keys even if cached.
|
||||||
* @param callback the asynchronous callback
|
|
||||||
*/
|
*/
|
||||||
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<MXDeviceInfo> {
|
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<MXDeviceInfo> {
|
||||||
Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds")
|
Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds")
|
||||||
|
@ -270,7 +253,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
Timber.v("## downloadKeys() : starts")
|
Timber.v("## downloadKeys() : starts")
|
||||||
val t0 = System.currentTimeMillis()
|
val t0 = System.currentTimeMillis()
|
||||||
val result = doKeyDownloadForUsers(downloadUsers)
|
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 {
|
result.also {
|
||||||
it.addEntriesFromMap(stored)
|
it.addEntriesFromMap(stored)
|
||||||
}
|
}
|
||||||
|
@ -303,16 +286,14 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
val devices = response.deviceKeys?.get(userId)
|
val devices = response.deviceKeys?.get(userId)
|
||||||
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices")
|
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices")
|
||||||
if (devices != null) {
|
if (devices != null) {
|
||||||
val mutableDevices = HashMap(devices)
|
val mutableDevices = devices.toMutableMap()
|
||||||
val deviceIds = ArrayList(mutableDevices.keys)
|
for ((deviceId, deviceInfo) in devices) {
|
||||||
for (deviceId in deviceIds) {
|
|
||||||
// Get the potential previously store device keys for this device
|
// Get the potential previously store device keys for this device
|
||||||
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
|
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
|
||||||
val deviceInfo = mutableDevices[deviceId]
|
|
||||||
|
|
||||||
// in some race conditions (like unit tests)
|
// in some race conditions (like unit tests)
|
||||||
// the self device must be seen as verified
|
// 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
|
deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED
|
||||||
}
|
}
|
||||||
// Validate received keys
|
// 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
|
// Check that the user_id and device_id in the received deviceKeys are correct
|
||||||
if (!TextUtils.equals(deviceKeys.userId, userId)) {
|
if (deviceKeys.userId != userId) {
|
||||||
Timber.e("## validateDeviceKeys() : Mismatched user_id " + deviceKeys.userId + " from " + userId + ":" + deviceId)
|
Timber.e("## validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TextUtils.equals(deviceKeys.deviceId, deviceId)) {
|
if (deviceKeys.deviceId != deviceId) {
|
||||||
Timber.e("## validateDeviceKeys() : Mismatched device_id " + deviceKeys.deviceId + " from " + userId + ":" + deviceId)
|
Timber.e("## validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -379,21 +360,21 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
val signKey = deviceKeys.keys?.get(signKeyId)
|
val signKey = deviceKeys.keys?.get(signKeyId)
|
||||||
|
|
||||||
if (null == signKey) {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val signatureMap = deviceKeys.signatures?.get(userId)
|
val signatureMap = deviceKeys.signatures?.get(userId)
|
||||||
|
|
||||||
if (null == signatureMap) {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val signature = signatureMap[signKeyId]
|
val signature = signatureMap[signKeyId]
|
||||||
|
|
||||||
if (null == signature) {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,7 +395,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null != previouslyStoredDeviceKeys) {
|
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
|
// This should only happen if the list has been MITMed; we are
|
||||||
// best off sticking with the original keys.
|
// best off sticking with the original keys.
|
||||||
//
|
//
|
||||||
|
@ -424,7 +405,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
+ previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey)
|
+ previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey)
|
||||||
|
|
||||||
Timber.e("## validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
|
Timber.e("## validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
|
||||||
Timber.e("## validateDeviceKeys() : " + previouslyStoredDeviceKeys.keys + " -> " + deviceKeys.keys)
|
Timber.e("## validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -438,27 +419,18 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
* This method must be called on getEncryptingThreadHandler() thread.
|
* This method must be called on getEncryptingThreadHandler() thread.
|
||||||
*/
|
*/
|
||||||
suspend fun refreshOutdatedDeviceLists() {
|
suspend fun refreshOutdatedDeviceLists() {
|
||||||
val users = ArrayList<String>()
|
|
||||||
|
|
||||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||||
|
|
||||||
for (userId in deviceTrackingStatuses.keys) {
|
val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId ->
|
||||||
if (TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]) {
|
TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]
|
||||||
users.add(userId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (users.size == 0) {
|
if (users.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the statuses
|
// update the statuses
|
||||||
for (userId in users) {
|
users.associateWithTo(deviceTrackingStatuses) { TRACKING_STATUS_DOWNLOAD_IN_PROGRESS }
|
||||||
val status = deviceTrackingStatuses[userId]
|
|
||||||
if (null != status && TRACKING_STATUS_PENDING_DOWNLOAD == status) {
|
|
||||||
deviceTrackingStatuses.put(userId, TRACKING_STATUS_DOWNLOAD_IN_PROGRESS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto
|
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.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
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.crypto.store.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
@ -58,7 +56,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
when (roomKeyShare?.action) {
|
when (roomKeyShare?.action) {
|
||||||
RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(event))
|
RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(event))
|
||||||
RoomKeyShare.ACTION_SHARE_CANCELLATION -> receivedRoomKeyRequestCancellations.add(IncomingRoomKeyRequestCancellation(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
|
* It must be called on CryptoThread
|
||||||
*/
|
*/
|
||||||
fun processReceivedRoomKeyRequests() {
|
fun processReceivedRoomKeyRequests() {
|
||||||
val roomKeyRequestsToProcess = ArrayList(receivedRoomKeyRequests)
|
val roomKeyRequestsToProcess = receivedRoomKeyRequests.toList()
|
||||||
receivedRoomKeyRequests.clear()
|
receivedRoomKeyRequests.clear()
|
||||||
for (request in roomKeyRequestsToProcess) {
|
for (request in roomKeyRequestsToProcess) {
|
||||||
val userId = request.userId
|
val userId = request.userId
|
||||||
|
@ -77,7 +75,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
val roomId = body!!.roomId
|
val roomId = body!!.roomId
|
||||||
val alg = body.algorithm
|
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) {
|
if (userId == null || credentials.userId != userId) {
|
||||||
// TODO: determine if we sent this device the keys already: in
|
// TODO: determine if we sent this device the keys already: in
|
||||||
Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now")
|
Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now")
|
||||||
|
@ -92,12 +90,12 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!decryptor.hasKeysForKeyRequest(request)) {
|
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)
|
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||||
continue
|
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")
|
Timber.v("## processReceivedRoomKeyRequests() : oneself device - ignored")
|
||||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||||
continue
|
continue
|
||||||
|
@ -132,7 +130,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
var receivedRoomKeyRequestCancellations: List<IncomingRoomKeyRequestCancellation>? = null
|
var receivedRoomKeyRequestCancellations: List<IncomingRoomKeyRequestCancellation>? = null
|
||||||
|
|
||||||
synchronized(this.receivedRoomKeyRequestCancellations) {
|
synchronized(this.receivedRoomKeyRequestCancellations) {
|
||||||
if (!this.receivedRoomKeyRequestCancellations.isEmpty()) {
|
if (this.receivedRoomKeyRequestCancellations.isNotEmpty()) {
|
||||||
receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList()
|
receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList()
|
||||||
this.receivedRoomKeyRequestCancellations.clear()
|
this.receivedRoomKeyRequestCancellations.clear()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,20 +16,19 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto
|
package im.vector.matrix.android.internal.crypto
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import im.vector.matrix.android.internal.extensions.toUnsignedInt
|
import im.vector.matrix.android.internal.extensions.toUnsignedInt
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
import javax.crypto.Mac
|
import javax.crypto.Mac
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
import kotlin.experimental.and
|
import kotlin.experimental.and
|
||||||
import kotlin.experimental.xor
|
import kotlin.experimental.xor
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class to import/export the crypto data
|
* Utility class to import/export the crypto data
|
||||||
|
@ -51,7 +50,7 @@ object MXMegolmExportEncryption {
|
||||||
* @return the AES key
|
* @return the AES key
|
||||||
*/
|
*/
|
||||||
private fun getAesKey(keyBits: ByteArray): ByteArray {
|
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.
|
* @return the Hmac key.
|
||||||
*/
|
*/
|
||||||
private fun getHmacKey(keyBits: ByteArray): ByteArray {
|
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)
|
val body = unpackMegolmKeyFile(data)
|
||||||
|
|
||||||
// check we have a version byte
|
// check we have a version byte
|
||||||
if (null == body || body.size == 0) {
|
if (null == body || body.isEmpty()) {
|
||||||
Timber.e("## decryptMegolmKeyFile() : Invalid file: too short")
|
Timber.e("## decryptMegolmKeyFile() : Invalid file: too short")
|
||||||
throw Exception("Invalid file: too short")
|
throw Exception("Invalid file: too short")
|
||||||
}
|
}
|
||||||
|
@ -93,27 +92,27 @@ object MXMegolmExportEncryption {
|
||||||
throw Exception("Invalid file: too short")
|
throw Exception("Invalid file: too short")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(password)) {
|
if (password.isEmpty()) {
|
||||||
throw Exception("Empty password is not supported")
|
throw Exception("Empty password is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
val salt = Arrays.copyOfRange(body, 1, 1 + 16)
|
val salt = body.copyOfRange(1, 1 + 16)
|
||||||
val iv = Arrays.copyOfRange(body, 17, 17 + 16)
|
val iv = body.copyOfRange(17, 17 + 16)
|
||||||
val iterations =
|
val iterations =
|
||||||
(body[33].toUnsignedInt() shl 24) or (body[34].toUnsignedInt() shl 16) or (body[35].toUnsignedInt() shl 8) or body[36].toUnsignedInt()
|
(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 ciphertext = body.copyOfRange(37, 37 + ciphertextLength)
|
||||||
val hmac = Arrays.copyOfRange(body, body.size - 32, body.size)
|
val hmac = body.copyOfRange(body.size - 32, body.size)
|
||||||
|
|
||||||
val deriveKey = deriveKeys(salt, iterations, password)
|
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 macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256")
|
||||||
val mac = Mac.getInstance("HmacSHA256")
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
mac.init(macKey)
|
mac.init(macKey)
|
||||||
val digest = mac.doFinal(toVerify)
|
val digest = mac.doFinal(toVerify)
|
||||||
|
|
||||||
if (!Arrays.equals(hmac, digest)) {
|
if (!hmac.contentEquals(digest)) {
|
||||||
Timber.e("## decryptMegolmKeyFile() : Authentication check failed: incorrect password?")
|
Timber.e("## decryptMegolmKeyFile() : Authentication check failed: incorrect password?")
|
||||||
throw Exception("Authentication check failed: incorrect password?")
|
throw Exception("Authentication check failed: incorrect password?")
|
||||||
}
|
}
|
||||||
|
@ -146,7 +145,7 @@ object MXMegolmExportEncryption {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun encryptMegolmKeyFile(data: String, password: String, kdf_rounds: Int = DEFAULT_ITERATION_COUNT): ByteArray {
|
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")
|
throw Exception("Empty password is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +195,7 @@ object MXMegolmExportEncryption {
|
||||||
System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.size)
|
System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.size)
|
||||||
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 macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256")
|
||||||
val mac = Mac.getInstance("HmacSHA256")
|
val mac = Mac.getInstance("HmacSHA256")
|
||||||
|
@ -234,7 +233,7 @@ object MXMegolmExportEncryption {
|
||||||
// start the next line after the newline
|
// start the next line after the newline
|
||||||
lineStart = lineEnd + 1
|
lineStart = lineEnd + 1
|
||||||
|
|
||||||
if (TextUtils.equals(line, HEADER_LINE)) {
|
if (line == HEADER_LINE) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,15 +243,13 @@ object MXMegolmExportEncryption {
|
||||||
// look for the end line
|
// look for the end line
|
||||||
while (true) {
|
while (true) {
|
||||||
val lineEnd = fileStr.indexOf('\n', lineStart)
|
val lineEnd = fileStr.indexOf('\n', lineStart)
|
||||||
val line: String
|
val line = if (lineEnd < 0) {
|
||||||
|
fileStr.substring(lineStart)
|
||||||
if (lineEnd < 0) {
|
|
||||||
line = fileStr.substring(lineStart).trim()
|
|
||||||
} else {
|
} else {
|
||||||
line = fileStr.substring(lineStart, lineEnd).trim()
|
fileStr.substring(lineStart, lineEnd)
|
||||||
}
|
}.trim()
|
||||||
|
|
||||||
if (TextUtils.equals(line, TRAILER_LINE)) {
|
if (line == TRAILER_LINE) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,7 +287,7 @@ object MXMegolmExportEncryption {
|
||||||
for (i in 1..nLines) {
|
for (i in 1..nLines) {
|
||||||
outStream.write("\n".toByteArray())
|
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))
|
outStream.write(Base64.encode(data, o, len, Base64.DEFAULT))
|
||||||
o += LINE_LENGTH
|
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.
|
// it is simpler than the generic algorithm because the expected key length is equal to the mac key length.
|
||||||
// noticed as dklen/hlen
|
// noticed as dklen/hlen
|
||||||
val prf = Mac.getInstance("HmacSHA512")
|
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
|
// 512 bits key length
|
||||||
val key = ByteArray(64)
|
val key = ByteArray(64)
|
||||||
|
@ -326,8 +323,7 @@ object MXMegolmExportEncryption {
|
||||||
|
|
||||||
// U1 = PRF(Password, Salt || INT_32_BE(i))
|
// U1 = PRF(Password, Salt || INT_32_BE(i))
|
||||||
prf.update(salt)
|
prf.update(salt)
|
||||||
val int32BE = ByteArray(4)
|
val int32BE = ByteArray(4) { 0.toByte() }
|
||||||
Arrays.fill(int32BE, 0.toByte())
|
|
||||||
int32BE[3] = 1.toByte()
|
int32BE[3] = 1.toByte()
|
||||||
prf.update(int32BE)
|
prf.update(int32BE)
|
||||||
prf.doFinal(Uc, 0)
|
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
|
return key
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto
|
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.session.crypto.MXCryptoError
|
||||||
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
||||||
import im.vector.matrix.android.api.util.JsonDict
|
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 org.matrix.olm.*
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
// The libolm wrapper.
|
// The libolm wrapper.
|
||||||
|
@ -434,7 +432,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @return the base64-encoded secret key.
|
* @return the base64-encoded secret key.
|
||||||
*/
|
*/
|
||||||
fun getSessionKey(sessionId: String): String? {
|
fun getSessionKey(sessionId: String): String? {
|
||||||
if (!TextUtils.isEmpty(sessionId)) {
|
if (sessionId.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
return outboundGroupSessionStore[sessionId]!!.sessionKey()
|
return outboundGroupSessionStore[sessionId]!!.sessionKey()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -451,7 +449,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @return the current chain index.
|
* @return the current chain index.
|
||||||
*/
|
*/
|
||||||
fun getMessageIndex(sessionId: String): Int {
|
fun getMessageIndex(sessionId: String): Int {
|
||||||
return if (!TextUtils.isEmpty(sessionId)) {
|
return if (sessionId.isNotEmpty()) {
|
||||||
outboundGroupSessionStore[sessionId]!!.messageIndex()
|
outboundGroupSessionStore[sessionId]!!.messageIndex()
|
||||||
} else 0
|
} else 0
|
||||||
}
|
}
|
||||||
|
@ -464,7 +462,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
* @return ciphertext
|
* @return ciphertext
|
||||||
*/
|
*/
|
||||||
fun encryptGroupMessage(sessionId: String, payloadString: String): String? {
|
fun encryptGroupMessage(sessionId: String, payloadString: String): String? {
|
||||||
if (!TextUtils.isEmpty(sessionId) && !TextUtils.isEmpty(payloadString)) {
|
if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
return outboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString)
|
return outboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -523,7 +521,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!TextUtils.equals(session.olmInboundGroupSession!!.sessionIdentifier(), sessionId)) {
|
if (session.olmInboundGroupSession!!.sessionIdentifier() != sessionId) {
|
||||||
Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||||
session.olmInboundGroupSession!!.releaseSession()
|
session.olmInboundGroupSession!!.releaseSession()
|
||||||
return false
|
return false
|
||||||
|
@ -573,7 +571,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!TextUtils.equals(session.olmInboundGroupSession?.sessionIdentifier(), sessionId)) {
|
if (session.olmInboundGroupSession?.sessionIdentifier() != sessionId) {
|
||||||
Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||||
if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession()
|
if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession()
|
||||||
continue
|
continue
|
||||||
|
@ -758,7 +756,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
// Check that the room id matches the original one for the session. This stops
|
// Check that the room id matches the original one for the session. This stops
|
||||||
// the HS pretending a message was targeting a different room.
|
// 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)
|
val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
||||||
Timber.e("## getInboundGroupSession() : $errorDescription")
|
Timber.e("## getInboundGroupSession() : $errorDescription")
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)
|
||||||
|
|
|
@ -16,12 +16,10 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto
|
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.auth.data.Credentials
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
|
@ -42,11 +40,11 @@ internal class MyDeviceInfoHolder @Inject constructor(
|
||||||
init {
|
init {
|
||||||
val keys = HashMap<String, String>()
|
val keys = HashMap<String, String>()
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(olmDevice.deviceEd25519Key)) {
|
if (!olmDevice.deviceEd25519Key.isNullOrEmpty()) {
|
||||||
keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!!
|
keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!!
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(olmDevice.deviceCurve25519Key)) {
|
if (!olmDevice.deviceCurve25519Key.isNullOrEmpty()) {
|
||||||
keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!!
|
keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,13 +56,7 @@ internal class MyDeviceInfoHolder @Inject constructor(
|
||||||
// Add our own deviceinfo to the store
|
// Add our own deviceinfo to the store
|
||||||
val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId)
|
val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId)
|
||||||
|
|
||||||
val myDevices: MutableMap<String, MXDeviceInfo>
|
val myDevices = endToEndDevicesForUser.orEmpty().toMutableMap()
|
||||||
|
|
||||||
if (null != endToEndDevicesForUser) {
|
|
||||||
myDevices = HashMap(endToEndDevicesForUser)
|
|
||||||
} else {
|
|
||||||
myDevices = HashMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
myDevices[myDevice.deviceId] = myDevice
|
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 im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||||
import org.matrix.olm.OlmAccount
|
import org.matrix.olm.OlmAccount
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class OneTimeKeysUploader @Inject constructor(
|
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
|
// If we run out of slots when generating new keys then olm will
|
||||||
// discard the oldest private keys first. This will eventually clean
|
// discard the oldest private keys first. This will eventually clean
|
||||||
// out stale private keys that won't receive a message.
|
// 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) {
|
if (oneTimeKeyCount != null) {
|
||||||
uploadOTK(oneTimeKeyCount!!, keyLimit)
|
uploadOTK(oneTimeKeyCount!!, keyLimit)
|
||||||
} else {
|
} else {
|
||||||
|
@ -116,7 +117,7 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||||
// If we don't need to generate any more keys then we are done.
|
// If we don't need to generate any more keys then we are done.
|
||||||
return
|
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)
|
olmDevice.generateOneTimeKeys(keysThisLoop)
|
||||||
val response = uploadOneTimeKeys()
|
val response = uploadOneTimeKeys()
|
||||||
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
|
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
|
||||||
|
@ -132,14 +133,14 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||||
*/
|
*/
|
||||||
private suspend fun uploadOneTimeKeys(): KeysUploadResponse {
|
private suspend fun uploadOneTimeKeys(): KeysUploadResponse {
|
||||||
val oneTimeKeys = olmDevice.getOneTimeKeys()
|
val oneTimeKeys = olmDevice.getOneTimeKeys()
|
||||||
val oneTimeJson = HashMap<String, Any>()
|
val oneTimeJson = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY)
|
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY)
|
||||||
|
|
||||||
if (null != curve25519Map) {
|
if (null != curve25519Map) {
|
||||||
for (key_id in curve25519Map.keys) {
|
for ((key_id, value) in curve25519Map) {
|
||||||
val k = HashMap<String, Any>()
|
val k = mutableMapOf<String, Any>()
|
||||||
k["key"] = curve25519Map.getValue(key_id)
|
k["key"] = value
|
||||||
|
|
||||||
// the key is also signed
|
// the key is also signed
|
||||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
|
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
|
||||||
|
|
|
@ -16,13 +16,11 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto
|
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.IMXDecrypting
|
||||||
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory
|
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.crypto.algorithms.olm.MXOlmDecryptionFactory
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
|
@ -62,10 +60,8 @@ internal class RoomDecryptorProvider @Inject constructor(
|
||||||
}
|
}
|
||||||
if (roomId != null && roomId.isNotEmpty()) {
|
if (roomId != null && roomId.isNotEmpty()) {
|
||||||
synchronized(roomDecryptors) {
|
synchronized(roomDecryptors) {
|
||||||
if (!roomDecryptors.containsKey(roomId)) {
|
val decryptors = roomDecryptors.getOrPut(roomId) { mutableMapOf() }
|
||||||
roomDecryptors[roomId] = HashMap()
|
val alg = decryptors[algorithm]
|
||||||
}
|
|
||||||
val alg = roomDecryptors[roomId]?.get(algorithm)
|
|
||||||
if (alg != null) {
|
if (alg != null) {
|
||||||
return alg
|
return alg
|
||||||
}
|
}
|
||||||
|
@ -89,7 +85,7 @@ internal class RoomDecryptorProvider @Inject constructor(
|
||||||
}
|
}
|
||||||
else -> olmDecryptionFactory.create()
|
else -> olmDecryptionFactory.create()
|
||||||
}
|
}
|
||||||
if (roomId != null && !TextUtils.isEmpty(roomId)) {
|
if (!roomId.isNullOrEmpty()) {
|
||||||
synchronized(roomDecryptors) {
|
synchronized(roomDecryptors) {
|
||||||
roomDecryptors[roomId]?.put(algorithm, alg)
|
roomDecryptors[roomId]?.put(algorithm, alg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.actions
|
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.MXOlmDevice
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXKey
|
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.model.MXUsersDevicesMap
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val olmDevice: MXOlmDevice,
|
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 results = MXUsersDevicesMap<MXOlmSessionResult>()
|
||||||
|
|
||||||
val userIds = devicesByUser.keys
|
for ((userId, deviceInfos) in devicesByUser) {
|
||||||
|
for (deviceInfo in deviceInfos) {
|
||||||
for (userId in userIds) {
|
|
||||||
val deviceInfos = devicesByUser[userId]
|
|
||||||
|
|
||||||
for (deviceInfo in deviceInfos!!) {
|
|
||||||
val deviceId = deviceInfo.deviceId
|
val deviceId = deviceInfo.deviceId
|
||||||
val key = deviceInfo.identityKey()
|
val key = deviceInfo.identityKey()
|
||||||
|
|
||||||
val sessionId = olmDevice.getSessionId(key!!)
|
val sessionId = olmDevice.getSessionId(key!!)
|
||||||
|
|
||||||
if (TextUtils.isEmpty(sessionId)) {
|
if (sessionId.isNullOrEmpty()) {
|
||||||
devicesWithoutSession.add(deviceInfo)
|
devicesWithoutSession.add(deviceInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,9 +73,8 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
|
||||||
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
|
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
|
||||||
val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams)
|
val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams)
|
||||||
Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
|
Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
|
||||||
for (userId in userIds) {
|
for ((userId, deviceInfos) in devicesByUser) {
|
||||||
val deviceInfos = devicesByUser[userId]
|
for (deviceInfo in deviceInfos) {
|
||||||
for (deviceInfo in deviceInfos!!) {
|
|
||||||
var oneTimeKey: MXKey? = null
|
var oneTimeKey: MXKey? = null
|
||||||
val deviceIds = oneTimeKeys.getUserDeviceIds(userId)
|
val deviceIds = oneTimeKeys.getUserDeviceIds(userId)
|
||||||
if (null != deviceIds) {
|
if (null != deviceIds) {
|
||||||
|
@ -116,24 +109,22 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(private val
|
||||||
val signKeyId = "ed25519:$deviceId"
|
val signKeyId = "ed25519:$deviceId"
|
||||||
val signature = oneTimeKey.signatureForUserId(userId, signKeyId)
|
val signature = oneTimeKey.signatureForUserId(userId, signKeyId)
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(signature) && !TextUtils.isEmpty(deviceInfo.fingerprint())) {
|
if (!signature.isNullOrEmpty() && !deviceInfo.fingerprint().isNullOrEmpty()) {
|
||||||
var isVerified = false
|
var isVerified = false
|
||||||
var errorMessage: String? = null
|
var errorMessage: String? = null
|
||||||
|
|
||||||
if (signature != null) {
|
|
||||||
try {
|
try {
|
||||||
olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature)
|
olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature)
|
||||||
isVerified = true
|
isVerified = true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errorMessage = e.message
|
errorMessage = e.message
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check one-time key signature
|
// Check one-time key signature
|
||||||
if (isVerified) {
|
if (isVerified) {
|
||||||
sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value)
|
sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value)
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(sessionId)) {
|
if (!sessionId.isNullOrEmpty()) {
|
||||||
Timber.v("## verifyKeyAndStartSession() : Started new sessionid " + sessionId
|
Timber.v("## verifyKeyAndStartSession() : Started new sessionid " + sessionId
|
||||||
+ " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")")
|
+ " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")")
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,14 +16,11 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.actions
|
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.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.MXOlmSessionResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val olmDevice: MXOlmDevice,
|
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> {
|
suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> {
|
||||||
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
|
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
|
||||||
val devicesByUser = HashMap<String /* userId */, MutableList<MXDeviceInfo>>()
|
val devicesByUser = users.associateWith { userId ->
|
||||||
|
|
||||||
for (userId in users) {
|
|
||||||
devicesByUser[userId] = ArrayList()
|
|
||||||
|
|
||||||
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
|
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
|
||||||
|
|
||||||
for (device in devices) {
|
devices.filter {
|
||||||
val key = device.identityKey()
|
|
||||||
|
|
||||||
if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) {
|
|
||||||
// Don't bother setting up session to ourself
|
// Don't bother setting up session to ourself
|
||||||
continue
|
it.identityKey() != olmDevice.deviceCurve25519Key
|
||||||
}
|
|
||||||
|
|
||||||
if (device.isVerified) {
|
|
||||||
// Don't bother setting up sessions with blocked users
|
// Don't bother setting up sessions with blocked users
|
||||||
continue
|
&& !it.isVerified
|
||||||
}
|
|
||||||
|
|
||||||
devicesByUser[userId]!!.add(device)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
return ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.actions
|
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.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
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.JsonCanonicalizer
|
||||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class MessageEncrypter @Inject constructor(private val credentials: Credentials,
|
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.
|
* @return the content for an m.room.encrypted event.
|
||||||
*/
|
*/
|
||||||
fun encryptMessage(payloadFields: Map<String, Any>, deviceInfos: List<MXDeviceInfo>): EncryptedMessage {
|
fun encryptMessage(payloadFields: Map<String, Any>, deviceInfos: List<MXDeviceInfo>): EncryptedMessage {
|
||||||
val deviceInfoParticipantKey = HashMap<String, MXDeviceInfo>()
|
val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
|
||||||
val participantKeys = ArrayList<String>()
|
|
||||||
|
|
||||||
for (di in deviceInfos) {
|
val payloadJson = payloadFields.toMutableMap()
|
||||||
participantKeys.add(di.identityKey()!!)
|
|
||||||
deviceInfoParticipantKey[di.identityKey()!!] = di
|
|
||||||
}
|
|
||||||
|
|
||||||
val payloadJson = HashMap(payloadFields)
|
|
||||||
|
|
||||||
payloadJson["sender"] = credentials.userId
|
payloadJson["sender"] = credentials.userId
|
||||||
payloadJson["sender_device"] = credentials.deviceId
|
payloadJson["sender_device"] = credentials.deviceId!!
|
||||||
|
|
||||||
// Include the Ed25519 key so that the recipient knows what
|
// Include the Ed25519 key so that the recipient knows what
|
||||||
// device this message came from.
|
// device this message came from.
|
||||||
|
@ -67,30 +59,24 @@ internal class MessageEncrypter @Inject constructor(private val credentials: Cre
|
||||||
|
|
||||||
val ciphertext = HashMap<String, Any>()
|
val ciphertext = HashMap<String, Any>()
|
||||||
|
|
||||||
for (deviceKey in participantKeys) {
|
for ((deviceKey, deviceInfo) in deviceInfoParticipantKey) {
|
||||||
val sessionId = olmDevice.getSessionId(deviceKey)
|
val sessionId = olmDevice.getSessionId(deviceKey)
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(sessionId)) {
|
if (!sessionId.isNullOrEmpty()) {
|
||||||
Timber.v("Using sessionid $sessionId for device $deviceKey")
|
Timber.v("Using sessionid $sessionId for device $deviceKey")
|
||||||
val deviceInfo = deviceInfoParticipantKey[deviceKey]
|
|
||||||
|
|
||||||
payloadJson["recipient"] = deviceInfo!!.userId
|
payloadJson["recipient"] = deviceInfo.userId
|
||||||
|
payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!)
|
||||||
val recipientsKeysMap = HashMap<String, String>()
|
|
||||||
recipientsKeysMap["ed25519"] = deviceInfo.fingerprint()!!
|
|
||||||
payloadJson["recipient_keys"] = recipientsKeysMap
|
|
||||||
|
|
||||||
val payloadString = convertToUTF8(JsonCanonicalizer.getCanonicalJson(Map::class.java, payloadJson))
|
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()
|
return EncryptedMessage(
|
||||||
|
algorithm = MXCRYPTO_ALGORITHM_OLM,
|
||||||
res.algorithm = MXCRYPTO_ALGORITHM_OLM
|
senderKey = olmDevice.deviceCurve25519Key,
|
||||||
res.senderKey = olmDevice.deviceCurve25519Key
|
cipherText = ciphertext
|
||||||
res.cipherText = ciphertext
|
)
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
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.crypto.MXCryptoError
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
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.EventType
|
||||||
|
@ -148,7 +147,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
selfMap["deviceId"] = "*"
|
selfMap["deviceId"] = "*"
|
||||||
recipients.add(selfMap)
|
recipients.add(selfMap)
|
||||||
|
|
||||||
if (!TextUtils.equals(sender, userId)) {
|
if (sender != userId) {
|
||||||
val senderMap = HashMap<String, String>()
|
val senderMap = HashMap<String, String>()
|
||||||
senderMap["userId"] = sender
|
senderMap["userId"] = sender
|
||||||
senderMap["deviceId"] = encryptedEventContent.deviceId!!
|
senderMap["deviceId"] = encryptedEventContent.deviceId!!
|
||||||
|
@ -176,17 +175,12 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
|
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
|
||||||
val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
|
val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
|
||||||
|
|
||||||
if (!pendingEvents.containsKey(pendingEventsKey)) {
|
val timeline = pendingEvents.getOrPut(pendingEventsKey) { HashMap() }
|
||||||
pendingEvents[pendingEventsKey] = HashMap()
|
val events = timeline.getOrPut(timelineId) { ArrayList() }
|
||||||
}
|
|
||||||
|
|
||||||
if (pendingEvents[pendingEventsKey]?.containsKey(timelineId) == false) {
|
if (event !in events) {
|
||||||
pendingEvents[pendingEventsKey]?.put(timelineId, ArrayList())
|
Timber.v("## addEventToPendingList() : add Event ${event.eventId} in room id ${event.roomId}")
|
||||||
}
|
events.add(event)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +197,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
var keysClaimed: MutableMap<String, String> = HashMap()
|
var keysClaimed: MutableMap<String, String> = HashMap()
|
||||||
val forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
|
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")
|
Timber.e("## onRoomKeyEvent() : Key event is missing fields")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -250,13 +244,6 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
keysClaimed = event.getKeysClaimed().toMutableMap()
|
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,
|
val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId,
|
||||||
roomKeyContent.sessionKey,
|
roomKeyContent.sessionKey,
|
||||||
roomKeyContent.roomId,
|
roomKeyContent.roomId,
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
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.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||||
import im.vector.matrix.android.api.session.events.model.Content
|
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.JsonCanonicalizer
|
||||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
internal class MXMegolmEncryption(
|
internal class MXMegolmEncryption(
|
||||||
// The id of the room we will be sending to.
|
// The id of the room we will be sending to.
|
||||||
|
@ -85,7 +83,7 @@ internal class MXMegolmEncryption(
|
||||||
keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!!
|
keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!!
|
||||||
|
|
||||||
olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
|
olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
|
||||||
ArrayList(), keysClaimedMap, false)
|
emptyList(), keysClaimedMap, false)
|
||||||
|
|
||||||
keysBackup.maybeBackupKeys()
|
keysBackup.maybeBackupKeys()
|
||||||
|
|
||||||
|
@ -115,10 +113,8 @@ internal class MXMegolmEncryption(
|
||||||
for (deviceId in deviceIds!!) {
|
for (deviceId in deviceIds!!) {
|
||||||
val deviceInfo = devicesInRoom.getObject(userId, deviceId)
|
val deviceInfo = devicesInRoom.getObject(userId, deviceId)
|
||||||
if (deviceInfo != null && null == safeSession.sharedWithDevices.getObject(userId, deviceId)) {
|
if (deviceInfo != null && null == safeSession.sharedWithDevices.getObject(userId, deviceId)) {
|
||||||
if (!shareMap.containsKey(userId)) {
|
val devices = shareMap.getOrPut(userId) { ArrayList() }
|
||||||
shareMap[userId] = ArrayList()
|
devices.add(deviceInfo)
|
||||||
}
|
|
||||||
shareMap[userId]!!.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)
|
// 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 subMap = HashMap<String, List<MXDeviceInfo>>()
|
||||||
val userIds = ArrayList<String>()
|
|
||||||
var devicesCount = 0
|
var devicesCount = 0
|
||||||
for (userId in devicesByUsers.keys) {
|
for ((userId, devices) in devicesByUsers) {
|
||||||
devicesByUsers[userId]?.let {
|
subMap[userId] = devices
|
||||||
userIds.add(userId)
|
devicesCount += devices.size
|
||||||
subMap[userId] = it
|
|
||||||
devicesCount += it.size
|
|
||||||
}
|
|
||||||
if (devicesCount > 100) {
|
if (devicesCount > 100) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Timber.v("## shareKey() ; userId $userIds")
|
Timber.v("## shareKey() ; userId ${subMap.keys}")
|
||||||
shareUserDevicesKey(session, subMap)
|
shareUserDevicesKey(session, subMap)
|
||||||
val remainingDevices = devicesByUsers.filterKeys { userIds.contains(it).not() }
|
val remainingDevices = devicesByUsers - subMap.keys
|
||||||
shareKey(session, remainingDevices)
|
shareKey(session, remainingDevices)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +156,6 @@ internal class MXMegolmEncryption(
|
||||||
*
|
*
|
||||||
* @param session the session info
|
* @param session the session info
|
||||||
* @param devicesByUser the devices map
|
* @param devicesByUser the devices map
|
||||||
* @param callback the asynchronous callback
|
|
||||||
*/
|
*/
|
||||||
private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo,
|
private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo,
|
||||||
devicesByUser: Map<String, List<MXDeviceInfo>>) {
|
devicesByUser: Map<String, List<MXDeviceInfo>>) {
|
||||||
|
@ -210,8 +201,7 @@ internal class MXMegolmEncryption(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
|
Timber.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
|
||||||
//noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument
|
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo)))
|
||||||
contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo)))
|
|
||||||
haveTargets = true
|
haveTargets = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,9 +218,8 @@ internal class MXMegolmEncryption(
|
||||||
// attempted to share with) rather than the contentMap (those we did
|
// 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
|
// share with), because we don't want to try to claim a one-time-key
|
||||||
// for dead devices on every message.
|
// for dead devices on every message.
|
||||||
for (userId in devicesByUser.keys) {
|
for ((userId, devicesToShareWith) in devicesByUser) {
|
||||||
val devicesToShareWith = devicesByUser[userId]
|
for ((deviceId) in devicesToShareWith) {
|
||||||
for ((deviceId) in devicesToShareWith!!) {
|
|
||||||
session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
|
session.sharedWithDevices.setObject(userId, deviceId, chainIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -272,7 +261,6 @@ internal class MXMegolmEncryption(
|
||||||
* This method must be called in getDecryptingThreadHandler() thread.
|
* This method must be called in getDecryptingThreadHandler() thread.
|
||||||
*
|
*
|
||||||
* @param userIds the user ids whose devices must be checked.
|
* @param userIds the user ids whose devices must be checked.
|
||||||
* @param callback the asynchronous callback
|
|
||||||
*/
|
*/
|
||||||
private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<MXDeviceInfo> {
|
private suspend fun getDevicesInRoom(userIds: List<String>): MXUsersDevicesMap<MXDeviceInfo> {
|
||||||
// We are happy to use a cached version here: we assume that if we already
|
// We are happy to use a cached version here: we assume that if we already
|
||||||
|
@ -304,7 +292,7 @@ internal class MXMegolmEncryption(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.equals(deviceInfo.identityKey(), olmDevice.deviceCurve25519Key)) {
|
if (deviceInfo.identityKey() == olmDevice.deviceCurve25519Key) {
|
||||||
// Don't bother sending to ourself
|
// Don't bother sending to ourself
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.algorithms.olm
|
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.Content
|
||||||
import im.vector.matrix.android.api.session.events.model.toContent
|
import im.vector.matrix.android.api.session.events.model.toContent
|
||||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
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.algorithms.IMXEncrypting
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
internal class MXOlmEncryption(
|
internal class MXOlmEncryption(
|
||||||
private var roomId: String,
|
private var roomId: String,
|
||||||
|
@ -49,7 +47,7 @@ internal class MXOlmEncryption(
|
||||||
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
|
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
|
||||||
for (device in devices) {
|
for (device in devices) {
|
||||||
val key = device.identityKey()
|
val key = device.identityKey()
|
||||||
if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) {
|
if (key == olmDevice.deviceCurve25519Key) {
|
||||||
// Don't bother setting up session to ourself
|
// Don't bother setting up session to ourself
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -61,13 +59,14 @@ internal class MXOlmEncryption(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val messageMap = HashMap<String, Any>()
|
val messageMap = mapOf(
|
||||||
messageMap["room_id"] = roomId
|
"room_id" to roomId,
|
||||||
messageMap["type"] = eventType
|
"type" to eventType,
|
||||||
messageMap["content"] = eventContent
|
"content" to eventContent
|
||||||
|
)
|
||||||
|
|
||||||
messageEncrypter.encryptMessage(messageMap, deviceInfos)
|
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.UiThread
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import arrow.core.Try
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
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.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
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.MoshiProvider
|
||||||
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import im.vector.matrix.android.internal.task.Task
|
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.task.configureWith
|
||||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
|
import im.vector.matrix.android.internal.util.awaitCallback
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -77,6 +78,7 @@ import kotlin.random.Random
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class KeysBackup @Inject constructor(
|
internal class KeysBackup @Inject constructor(
|
||||||
|
@UserId private val userId: String,
|
||||||
private val credentials: Credentials,
|
private val credentials: Credentials,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val olmDevice: MXOlmDevice,
|
private val olmDevice: MXOlmDevice,
|
||||||
|
@ -142,8 +144,8 @@ internal class KeysBackup @Inject constructor(
|
||||||
progressListener: ProgressListener?,
|
progressListener: ProgressListener?,
|
||||||
callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
GlobalScope.launch(coroutineDispatchers.main) {
|
||||||
|
runCatching {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
withContext(coroutineDispatchers.crypto) {
|
||||||
Try {
|
|
||||||
val olmPkDecryption = OlmPkDecryption()
|
val olmPkDecryption = OlmPkDecryption()
|
||||||
val megolmBackupAuthData = MegolmBackupAuthData()
|
val megolmBackupAuthData = MegolmBackupAuthData()
|
||||||
|
|
||||||
|
@ -375,8 +377,6 @@ internal class KeysBackup @Inject constructor(
|
||||||
*/
|
*/
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
|
private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
|
||||||
val myUserId = credentials.userId
|
|
||||||
|
|
||||||
val keysBackupVersionTrust = KeysBackupVersionTrust()
|
val keysBackupVersionTrust = KeysBackupVersionTrust()
|
||||||
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
|
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
|
||||||
|
|
||||||
|
@ -388,13 +388,13 @@ internal class KeysBackup @Inject constructor(
|
||||||
return keysBackupVersionTrust
|
return keysBackupVersionTrust
|
||||||
}
|
}
|
||||||
|
|
||||||
val mySigs = authData.signatures?.get(myUserId)
|
val mySigs = authData.signatures?.get(userId)
|
||||||
if (mySigs.isNullOrEmpty()) {
|
if (mySigs.isNullOrEmpty()) {
|
||||||
Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user")
|
Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user")
|
||||||
return keysBackupVersionTrust
|
return keysBackupVersionTrust
|
||||||
}
|
}
|
||||||
|
|
||||||
for (keyId in mySigs.keys) {
|
for ((keyId, mySignature) in mySigs) {
|
||||||
// XXX: is this how we're supposed to get the device id?
|
// XXX: is this how we're supposed to get the device id?
|
||||||
var deviceId: String? = null
|
var deviceId: String? = null
|
||||||
val components = keyId.split(":")
|
val components = keyId.split(":")
|
||||||
|
@ -403,7 +403,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deviceId != null) {
|
if (deviceId != null) {
|
||||||
val device = cryptoStore.getUserDevice(deviceId, myUserId)
|
val device = cryptoStore.getUserDevice(deviceId, userId)
|
||||||
var isSignatureValid = false
|
var isSignatureValid = false
|
||||||
|
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
|
@ -412,7 +412,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
val fingerprint = device.fingerprint()
|
val fingerprint = device.fingerprint()
|
||||||
if (fingerprint != null) {
|
if (fingerprint != null) {
|
||||||
try {
|
try {
|
||||||
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySigs[keyId] as String)
|
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySignature)
|
||||||
isSignatureValid = true
|
isSignatureValid = true
|
||||||
} catch (e: OlmException) {
|
} catch (e: OlmException) {
|
||||||
Timber.v(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}")
|
Timber.v(e, "getKeysBackupTrust: Bad signature from device ${device.deviceId}")
|
||||||
|
@ -450,10 +450,8 @@ internal class KeysBackup @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
GlobalScope.launch(coroutineDispatchers.main) {
|
||||||
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
|
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
|
||||||
val myUserId = credentials.userId
|
|
||||||
|
|
||||||
// Get current signatures, or create an empty set
|
// Get current signatures, or create an empty set
|
||||||
val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap()
|
val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap()
|
||||||
?: HashMap()
|
?: HashMap()
|
||||||
|
|
||||||
if (trust) {
|
if (trust) {
|
||||||
|
@ -462,7 +460,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
|
|
||||||
val deviceSignatures = objectSigner.signObject(canonicalJson)
|
val deviceSignatures = objectSigner.signObject(canonicalJson)
|
||||||
|
|
||||||
deviceSignatures[myUserId]?.forEach { entry ->
|
deviceSignatures[userId]?.forEach { entry ->
|
||||||
myUserSignatures[entry.key] = entry.value
|
myUserSignatures[entry.key] = entry.value
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -478,7 +476,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
val newMegolmBackupAuthData = authData.copy()
|
val newMegolmBackupAuthData = authData.copy()
|
||||||
|
|
||||||
val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap()
|
val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap()
|
||||||
newSignatures[myUserId] = myUserSignatures
|
newSignatures[userId] = myUserSignatures
|
||||||
|
|
||||||
newMegolmBackupAuthData.signatures = newSignatures
|
newMegolmBackupAuthData.signatures = newSignatures
|
||||||
|
|
||||||
|
@ -617,8 +615,8 @@ internal class KeysBackup @Inject constructor(
|
||||||
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
||||||
|
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
GlobalScope.launch(coroutineDispatchers.main) {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
runCatching {
|
||||||
Try<OlmPkDecryption> {
|
val decryption = withContext(coroutineDispatchers.crypto) {
|
||||||
// Check if the recovery is valid before going any further
|
// Check if the recovery is valid before going any further
|
||||||
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
|
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
|
||||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
||||||
|
@ -626,36 +624,27 @@ internal class KeysBackup @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a PK decryption instance
|
// Get a PK decryption instance
|
||||||
val decryption = pkDecryptionFromRecoveryKey(recoveryKey)
|
pkDecryptionFromRecoveryKey(recoveryKey)
|
||||||
|
}
|
||||||
if (decryption == null) {
|
if (decryption == null) {
|
||||||
// This should not happen anymore
|
// This should not happen anymore
|
||||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
|
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
|
||||||
throw InvalidParameterException("Invalid recovery key")
|
throw InvalidParameterException("Invalid recovery key")
|
||||||
}
|
}
|
||||||
|
|
||||||
decryption
|
|
||||||
}
|
|
||||||
}.fold(
|
|
||||||
{
|
|
||||||
callback.onFailure(it)
|
|
||||||
},
|
|
||||||
{ decryption ->
|
|
||||||
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
|
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
|
||||||
|
|
||||||
// Get backed up keys from the homeserver
|
// Get backed up keys from the homeserver
|
||||||
getKeys(sessionId, roomId, keysVersionResult.version!!, object : MatrixCallback<KeysBackupData> {
|
val data = getKeys(sessionId, roomId, keysVersionResult.version!!)
|
||||||
override fun onSuccess(data: KeysBackupData) {
|
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
withContext(coroutineDispatchers.crypto) {
|
||||||
val importRoomKeysResult = withContext(coroutineDispatchers.crypto) {
|
|
||||||
val sessionsData = ArrayList<MegolmSessionData>()
|
val sessionsData = ArrayList<MegolmSessionData>()
|
||||||
// Restore that data
|
// Restore that data
|
||||||
var sessionsFromHsCount = 0
|
var sessionsFromHsCount = 0
|
||||||
for (roomIdLoop in data.roomIdToRoomKeysBackupData.keys) {
|
for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) {
|
||||||
for (sessionIdLoop in data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData.keys) {
|
for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) {
|
||||||
sessionsFromHsCount++
|
sessionsFromHsCount++
|
||||||
|
|
||||||
val keyBackupData = data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData[sessionIdLoop]!!
|
|
||||||
|
|
||||||
val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption)
|
val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption)
|
||||||
|
|
||||||
sessionData?.let {
|
sessionData?.let {
|
||||||
|
@ -694,17 +683,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
}.foldToCallback(callback)
|
||||||
callback.onSuccess(importRoomKeysResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
callback.onFailure(failure)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -717,7 +696,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
|
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
|
||||||
|
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
GlobalScope.launch(coroutineDispatchers.main) {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
runCatching {
|
||||||
val progressListener = if (stepProgressListener != null) {
|
val progressListener = if (stepProgressListener != null) {
|
||||||
object : ProgressListener {
|
object : ProgressListener {
|
||||||
override fun onProgress(progress: Int, total: Int) {
|
override fun onProgress(progress: Int, total: Int) {
|
||||||
|
@ -730,22 +709,18 @@ internal class KeysBackup @Inject constructor(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
Try {
|
val recoveryKey = withContext(coroutineDispatchers.crypto) {
|
||||||
recoveryKeyFromPassword(password, keysBackupVersion, progressListener)
|
recoveryKeyFromPassword(password, keysBackupVersion, progressListener)
|
||||||
}
|
}
|
||||||
}.fold(
|
|
||||||
{
|
|
||||||
callback.onFailure(it)
|
|
||||||
},
|
|
||||||
{ recoveryKey ->
|
|
||||||
if (recoveryKey == null) {
|
if (recoveryKey == null) {
|
||||||
Timber.v("backupKeys: Invalid configuration")
|
Timber.v("backupKeys: Invalid configuration")
|
||||||
callback.onFailure(IllegalStateException("Invalid configuration"))
|
throw IllegalStateException("Invalid configuration")
|
||||||
} else {
|
} else {
|
||||||
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, callback)
|
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
|
* Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable
|
||||||
* parameters and always returns a KeysBackupData object through the Callback
|
* parameters and always returns a KeysBackupData object through the Callback
|
||||||
*/
|
*/
|
||||||
private fun getKeys(sessionId: String?,
|
private suspend fun getKeys(sessionId: String?,
|
||||||
roomId: String?,
|
roomId: String?,
|
||||||
version: String,
|
version: String): KeysBackupData {
|
||||||
callback: MatrixCallback<KeysBackupData>) {
|
return if (roomId != null && sessionId != null) {
|
||||||
if (roomId != null && sessionId != null) {
|
|
||||||
// Get key for the room and for the session
|
// Get key for the room and for the session
|
||||||
getRoomSessionDataTask
|
val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version))
|
||||||
.configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version)) {
|
|
||||||
this.callback = object : MatrixCallback<KeyBackupData> {
|
|
||||||
override fun onSuccess(data: KeyBackupData) {
|
|
||||||
// Convert to KeysBackupData
|
// Convert to KeysBackupData
|
||||||
val keysBackupData = KeysBackupData()
|
KeysBackupData(mutableMapOf(
|
||||||
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
|
roomId to RoomKeysBackupData(mutableMapOf(
|
||||||
val roomKeysBackupData = RoomKeysBackupData()
|
sessionId to data
|
||||||
roomKeysBackupData.sessionIdToKeyBackupData = HashMap()
|
))
|
||||||
roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data
|
))
|
||||||
keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData
|
|
||||||
|
|
||||||
callback.onSuccess(keysBackupData)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
callback.onFailure(failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
} else if (roomId != null) {
|
} else if (roomId != null) {
|
||||||
// Get all keys for the room
|
// Get all keys for the room
|
||||||
getRoomSessionsDataTask
|
val data = getRoomSessionsDataTask.execute(GetRoomSessionsDataTask.Params(roomId, version))
|
||||||
.configureWith(GetRoomSessionsDataTask.Params(roomId, version)) {
|
|
||||||
this.callback = object : MatrixCallback<RoomKeysBackupData> {
|
|
||||||
override fun onSuccess(data: RoomKeysBackupData) {
|
|
||||||
// Convert to KeysBackupData
|
// Convert to KeysBackupData
|
||||||
val keysBackupData = KeysBackupData()
|
KeysBackupData(mutableMapOf(roomId to data))
|
||||||
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
|
|
||||||
keysBackupData.roomIdToRoomKeysBackupData[roomId] = data
|
|
||||||
|
|
||||||
callback.onSuccess(keysBackupData)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
callback.onFailure(failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
} else {
|
} else {
|
||||||
// Get all keys
|
// Get all keys
|
||||||
getSessionsDataTask
|
getSessionsDataTask.execute(GetSessionsDataTask.Params(version))
|
||||||
.configureWith(GetSessionsDataTask.Params(version)) {
|
|
||||||
this.callback = callback
|
|
||||||
}
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1411,5 +1352,5 @@ internal class KeysBackup @Inject constructor(
|
||||||
* DEBUG INFO
|
* 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
|
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.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
||||||
import org.matrix.olm.OlmInboundGroupSession
|
import org.matrix.olm.OlmInboundGroupSession
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class adds more context to a OlmInboundGroupSession object.
|
* This class adds more context to a OlmInboundGroupSession object.
|
||||||
|
@ -91,7 +89,7 @@ class OlmInboundGroupSessionWrapper : Serializable {
|
||||||
try {
|
try {
|
||||||
olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!)
|
olmInboundGroupSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!)
|
||||||
|
|
||||||
if (!TextUtils.equals(olmInboundGroupSession!!.sessionIdentifier(), megolmSessionData.sessionId)) {
|
if (olmInboundGroupSession!!.sessionIdentifier() != megolmSessionData.sessionId) {
|
||||||
throw Exception("Mismatched group session Id")
|
throw Exception("Mismatched group session Id")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.store.db
|
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.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||||
|
@ -101,8 +100,8 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
|
||||||
// Check credentials
|
// Check credentials
|
||||||
// The device id may not have been provided in credentials.
|
// The device id may not have been provided in credentials.
|
||||||
// Check it only if provided, else trust the stored one.
|
// Check it only if provided, else trust the stored one.
|
||||||
if (!TextUtils.equals(currentMetadata.userId, credentials.userId)
|
if (currentMetadata.userId != credentials.userId
|
||||||
|| (credentials.deviceId != null && !TextUtils.equals(credentials.deviceId, currentMetadata.deviceId))) {
|
|| (credentials.deviceId != null && credentials.deviceId != currentMetadata.deviceId)) {
|
||||||
Timber.w("## open() : Credentials do not match, close this store and delete data")
|
Timber.w("## open() : Credentials do not match, close this store and delete data")
|
||||||
deleteAll = true
|
deleteAll = true
|
||||||
currentMetadata = null
|
currentMetadata = null
|
||||||
|
|
|
@ -44,12 +44,9 @@ internal class DefaultClaimOneTimeKeysForUsersDevice @Inject constructor(private
|
||||||
}
|
}
|
||||||
val map = MXUsersDevicesMap<MXKey>()
|
val map = MXUsersDevicesMap<MXKey>()
|
||||||
keysClaimResponse.oneTimeKeys?.let { oneTimeKeys ->
|
keysClaimResponse.oneTimeKeys?.let { oneTimeKeys ->
|
||||||
for (userId in oneTimeKeys.keys) {
|
for ((userId, mapByUserId) in oneTimeKeys) {
|
||||||
val mapByUserId = oneTimeKeys[userId]
|
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) {
|
if (mxKey != null) {
|
||||||
map.setObject(userId, deviceId, mxKey)
|
map.setObject(userId, deviceId, mxKey)
|
||||||
|
@ -59,7 +56,6 @@ internal class DefaultClaimOneTimeKeysForUsersDevice @Inject constructor(private
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,11 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.tasks
|
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.api.CryptoApi
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryBody
|
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.crypto.model.rest.KeysQueryResponse
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface DownloadKeysForUsersTask : Task<DownloadKeysForUsersTask.Params, KeysQueryResponse> {
|
internal interface DownloadKeysForUsersTask : Task<DownloadKeysForUsersTask.Params, KeysQueryResponse> {
|
||||||
|
@ -37,19 +35,13 @@ internal class DefaultDownloadKeysForUsers @Inject constructor(private val crypt
|
||||||
: DownloadKeysForUsersTask {
|
: DownloadKeysForUsersTask {
|
||||||
|
|
||||||
override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse {
|
override suspend fun execute(params: DownloadKeysForUsersTask.Params): KeysQueryResponse {
|
||||||
val downloadQuery = HashMap<String, Map<String, Any>>()
|
val downloadQuery = params.userIds?.associateWith { emptyMap<String, Any>() }.orEmpty()
|
||||||
|
|
||||||
if (null != params.userIds) {
|
|
||||||
for (userId in params.userIds) {
|
|
||||||
downloadQuery[userId] = HashMap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val body = KeysQueryBody(
|
val body = KeysQueryBody(
|
||||||
deviceKeys = downloadQuery
|
deviceKeys = downloadQuery
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(params.token)) {
|
if (!params.token.isNullOrEmpty()) {
|
||||||
body.token = params.token
|
body.token = params.token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.crypto.tasks
|
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.api.CryptoApi
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.UpdateDeviceInfoBody
|
import im.vector.matrix.android.internal.crypto.model.rest.UpdateDeviceInfoBody
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
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) {
|
override suspend fun execute(params: SetDeviceNameTask.Params) {
|
||||||
val body = UpdateDeviceInfoBody(
|
val body = UpdateDeviceInfoBody(
|
||||||
displayName = if (TextUtils.isEmpty(params.deviceName)) "" else params.deviceName
|
displayName = params.deviceName
|
||||||
)
|
)
|
||||||
return executeRequest {
|
return executeRequest {
|
||||||
apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body)
|
apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body)
|
||||||
|
|
|
@ -124,7 +124,7 @@ internal fun ChunkEntity.add(roomId: String,
|
||||||
backwardsDisplayIndex = currentDisplayIndex
|
backwardsDisplayIndex = currentDisplayIndex
|
||||||
}
|
}
|
||||||
var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset)
|
var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset)
|
||||||
if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.getClearType())) {
|
if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) {
|
||||||
currentStateIndex += 1
|
currentStateIndex += 1
|
||||||
forwardsStateIndex = currentStateIndex
|
forwardsStateIndex = currentStateIndex
|
||||||
} else if (direction == PaginationDirection.BACKWARDS && timelineEvents.isNotEmpty()) {
|
} else if (direction == PaginationDirection.BACKWARDS && timelineEvents.isNotEmpty()) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ internal object EventMapper {
|
||||||
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
|
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
|
||||||
eventEntity.prevContent = ContentMapper.map(resolvedPrevContent)
|
eventEntity.prevContent = ContentMapper.map(resolvedPrevContent)
|
||||||
eventEntity.stateKey = event.stateKey
|
eventEntity.stateKey = event.stateKey
|
||||||
eventEntity.type = event.getClearType()
|
eventEntity.type = event.type
|
||||||
eventEntity.sender = event.senderId
|
eventEntity.sender = event.senderId
|
||||||
eventEntity.originServerTs = event.originServerTs
|
eventEntity.originServerTs = event.originServerTs
|
||||||
eventEntity.redacts = event.redacts
|
eventEntity.redacts = event.redacts
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
package im.vector.matrix.android.internal.network
|
package im.vector.matrix.android.internal.network
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.TextUtils
|
|
||||||
import im.vector.matrix.android.BuildConfig
|
import im.vector.matrix.android.BuildConfig
|
||||||
import im.vector.matrix.android.internal.di.MatrixScope
|
import im.vector.matrix.android.internal.di.MatrixScope
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -60,10 +59,10 @@ internal class UserAgentHolder @Inject constructor(private val context: Context)
|
||||||
Timber.e(e, "## initUserAgent() : failed")
|
Timber.e(e, "## initUserAgent() : failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
var systemUserAgent = System.getProperty("http.agent")
|
val systemUserAgent = System.getProperty("http.agent")
|
||||||
|
|
||||||
// cannot retrieve the application version
|
// cannot retrieve the application version
|
||||||
if (TextUtils.isEmpty(appName) || TextUtils.isEmpty(appVersion)) {
|
if (appName.isEmpty() || appVersion.isEmpty()) {
|
||||||
if (null == systemUserAgent) {
|
if (null == systemUserAgent) {
|
||||||
userAgent = "Java" + System.getProperty("java.version")
|
userAgent = "Java" + System.getProperty("java.version")
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,9 +75,7 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
|
||||||
private fun updateState(key: String, state: ContentUploadStateTracker.State) {
|
private fun updateState(key: String, state: ContentUploadStateTracker.State) {
|
||||||
states[key] = state
|
states[key] = state
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
listeners[key]?.also { listeners ->
|
listeners[key]?.forEach { it.onUpdate(state) }
|
||||||
listeners.forEach { it.onUpdate(state) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,13 +65,11 @@ internal class DefaultGetGroupDataTask @Inject constructor(
|
||||||
groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name
|
groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupId else name
|
||||||
groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription ?: ""
|
groupSummaryEntity.shortDescription = groupSummary.profile?.shortDescription ?: ""
|
||||||
|
|
||||||
val roomIds = groupRooms.rooms.map { it.roomId }
|
|
||||||
groupSummaryEntity.roomIds.clear()
|
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.clear()
|
||||||
groupSummaryEntity.userIds.addAll(userIds)
|
groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId }
|
||||||
|
|
||||||
groupSummaryEntity.membership = when (groupSummary.user?.membership) {
|
groupSummaryEntity.membership = when (groupSummary.user?.membership) {
|
||||||
Membership.JOIN.value -> Membership.JOIN
|
Membership.JOIN.value -> Membership.JOIN
|
||||||
|
|
|
@ -50,19 +50,15 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
||||||
defaultPushRuleService.dispatchRoomJoined(it)
|
defaultPushRuleService.dispatchRoomJoined(it)
|
||||||
}
|
}
|
||||||
val newJoinEvents = params.syncResponse.join
|
val newJoinEvents = params.syncResponse.join
|
||||||
.map { entries ->
|
.mapNotNull { (key, value) ->
|
||||||
entries.value.timeline?.events?.map { it.copy(roomId = entries.key) }
|
value.timeline?.events?.map { it.copy(roomId = key) }
|
||||||
}
|
}
|
||||||
.fold(emptyList<Event>(), { acc, next ->
|
.flatten()
|
||||||
acc + (next ?: emptyList())
|
|
||||||
})
|
|
||||||
val inviteEvents = params.syncResponse.invite
|
val inviteEvents = params.syncResponse.invite
|
||||||
.map { entries ->
|
.mapNotNull { (key, value) ->
|
||||||
entries.value.inviteState?.events?.map { it.copy(roomId = entries.key) }
|
value.inviteState?.events?.map { it.copy(roomId = key) }
|
||||||
}
|
}
|
||||||
.fold(emptyList<Event>(), { acc, next ->
|
.flatten()
|
||||||
acc + (next ?: emptyList())
|
|
||||||
})
|
|
||||||
val allEvents = (newJoinEvents + inviteEvents).filter { event ->
|
val allEvents = (newJoinEvents + inviteEvents).filter { event ->
|
||||||
when (event.type) {
|
when (event.type) {
|
||||||
EventType.MESSAGE,
|
EventType.MESSAGE,
|
||||||
|
@ -84,16 +80,12 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val allRedactedEvents = params.syncResponse.join
|
val allRedactedEvents = params.syncResponse.join
|
||||||
.map { entries ->
|
.asSequence()
|
||||||
entries.value.timeline?.events?.filter {
|
.mapNotNull { (_, value) -> value.timeline?.events }
|
||||||
it.type == EventType.REDACTION
|
.flatten()
|
||||||
}
|
.filter { it.type == EventType.REDACTION }
|
||||||
.orEmpty()
|
|
||||||
.mapNotNull { it.redacts }
|
.mapNotNull { it.redacts }
|
||||||
}
|
.toList()
|
||||||
.fold(emptyList<String>(), { acc, next ->
|
|
||||||
acc + next
|
|
||||||
})
|
|
||||||
|
|
||||||
Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events")
|
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? {
|
private fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule? {
|
||||||
// TODO This should be injected
|
// TODO This should be injected
|
||||||
val conditionResolver = DefaultConditionResolver(event, roomService, userId)
|
val conditionResolver = DefaultConditionResolver(event, roomService, userId)
|
||||||
rules.filter { it.enabled }.forEach { rule ->
|
return rules.firstOrNull { rule ->
|
||||||
val isFullfilled = rule.conditions?.map {
|
|
||||||
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.
|
// All conditions must hold true for an event in order to apply the action for the event.
|
||||||
acc && next
|
rule.enabled && rule.conditions?.all {
|
||||||
}) ?: false
|
it.asExecutableCondition()?.isSatisfied(conditionResolver) ?: false
|
||||||
|
} ?: false
|
||||||
if (isFullfilled) {
|
|
||||||
return rule
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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.members.MembershipService
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
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.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.read.ReadService
|
||||||
import im.vector.matrix.android.api.session.room.send.DraftService
|
import im.vector.matrix.android.api.session.room.send.DraftService
|
||||||
import im.vector.matrix.android.api.session.room.send.SendService
|
import im.vector.matrix.android.api.session.room.send.SendService
|
||||||
|
@ -44,15 +45,17 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
||||||
private val sendService: SendService,
|
private val sendService: SendService,
|
||||||
private val draftService: DraftService,
|
private val draftService: DraftService,
|
||||||
private val stateService: StateService,
|
private val stateService: StateService,
|
||||||
|
private val reportingService: ReportingService,
|
||||||
private val readService: ReadService,
|
private val readService: ReadService,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val relationService: RelationService,
|
private val relationService: RelationService,
|
||||||
private val roomMembersService: MembershipService
|
private val roomMembersService: MembershipService) :
|
||||||
) : Room,
|
Room,
|
||||||
TimelineService by timelineService,
|
TimelineService by timelineService,
|
||||||
SendService by sendService,
|
SendService by sendService,
|
||||||
DraftService by draftService,
|
DraftService by draftService,
|
||||||
StateService by stateService,
|
StateService by stateService,
|
||||||
|
ReportingService by reportingService,
|
||||||
ReadService by readService,
|
ReadService by readService,
|
||||||
RelationService by relationService,
|
RelationService by relationService,
|
||||||
MembershipService by roomMembersService {
|
MembershipService by roomMembersService {
|
||||||
|
|
|
@ -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.database.query.where
|
||||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
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.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.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
|
@ -41,6 +42,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
|
||||||
private val roomSummaryMapper: RoomSummaryMapper,
|
private val roomSummaryMapper: RoomSummaryMapper,
|
||||||
private val createRoomTask: CreateRoomTask,
|
private val createRoomTask: CreateRoomTask,
|
||||||
private val joinRoomTask: JoinRoomTask,
|
private val joinRoomTask: JoinRoomTask,
|
||||||
|
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
|
||||||
private val roomFactory: RoomFactory,
|
private val roomFactory: RoomFactory,
|
||||||
private val taskExecutor: TaskExecutor) : RoomService {
|
private val taskExecutor: TaskExecutor) : RoomService {
|
||||||
|
|
||||||
|
@ -80,4 +82,12 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.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.RoomMembersResponse
|
||||||
import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody
|
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.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.send.SendResponse
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
|
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
|
||||||
|
@ -245,4 +246,16 @@ internal interface RoomAPI {
|
||||||
@Path("eventId") parent_id: String,
|
@Path("eventId") parent_id: String,
|
||||||
@Body reason: Map<String, String>
|
@Body reason: Map<String, String>
|
||||||
): Call<SendResponse>
|
): 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.membership.DefaultMembershipService
|
||||||
import im.vector.matrix.android.internal.session.room.read.DefaultReadService
|
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.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.send.DefaultSendService
|
||||||
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
|
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
|
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 sendServiceFactory: DefaultSendService.Factory,
|
||||||
private val draftServiceFactory: DefaultDraftService.Factory,
|
private val draftServiceFactory: DefaultDraftService.Factory,
|
||||||
private val stateServiceFactory: DefaultStateService.Factory,
|
private val stateServiceFactory: DefaultStateService.Factory,
|
||||||
|
private val reportingServiceFactory: DefaultReportingService.Factory,
|
||||||
private val readServiceFactory: DefaultReadService.Factory,
|
private val readServiceFactory: DefaultReadService.Factory,
|
||||||
private val relationServiceFactory: DefaultRelationService.Factory,
|
private val relationServiceFactory: DefaultRelationService.Factory,
|
||||||
private val membershipServiceFactory: DefaultMembershipService.Factory) :
|
private val membershipServiceFactory: DefaultMembershipService.Factory) :
|
||||||
|
@ -54,6 +56,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
||||||
sendServiceFactory.create(roomId),
|
sendServiceFactory.create(roomId),
|
||||||
draftServiceFactory.create(roomId),
|
draftServiceFactory.create(roomId),
|
||||||
stateServiceFactory.create(roomId),
|
stateServiceFactory.create(roomId),
|
||||||
|
reportingServiceFactory.create(roomId),
|
||||||
readServiceFactory.create(roomId),
|
readServiceFactory.create(roomId),
|
||||||
cryptoService,
|
cryptoService,
|
||||||
relationServiceFactory.create(roomId),
|
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.membership.leaving.LeaveRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask
|
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.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.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.read.SetReadMarkersTask
|
||||||
import im.vector.matrix.android.internal.session.room.relation.DefaultFetchEditHistoryTask
|
import im.vector.matrix.android.internal.session.room.relation.*
|
||||||
import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask
|
import im.vector.matrix.android.internal.session.room.reporting.DefaultReportContentTask
|
||||||
import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask
|
import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask
|
||||||
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.state.DefaultSendStateTask
|
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.state.SendStateTask
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.*
|
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
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
@ -110,6 +104,9 @@ internal abstract class RoomModule {
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSetReadMarkersTask(setReadMarkersTask: DefaultSetReadMarkersTask): SetReadMarkersTask
|
abstract fun bindSetReadMarkersTask(setReadMarkersTask: DefaultSetReadMarkersTask): SetReadMarkersTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindMarkAllRoomsReadTask(markAllRoomsReadTask: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindFindReactionEventForUndoTask(findReactionEventForUndoTask: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask
|
abstract fun bindFindReactionEventForUndoTask(findReactionEventForUndoTask: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask
|
||||||
|
|
||||||
|
@ -119,6 +116,9 @@ internal abstract class RoomModule {
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask
|
abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindReportContentTask(reportContentTask: DefaultReportContentTask): ReportContentTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask
|
abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask
|
||||||
|
|
||||||
|
|
|
@ -132,8 +132,7 @@ internal class RoomMembers(private val realm: Realm,
|
||||||
.findAll()
|
.findAll()
|
||||||
.map { it.asDomain() }
|
.map { it.asDomain() }
|
||||||
.associateBy { it.stateKey!! }
|
.associateBy { it.stateKey!! }
|
||||||
.mapValues { it.value.content.toModel<RoomMember>()!! }
|
.filterValues { predicate(it.content.toModel<RoomMember>()!!) }
|
||||||
.filterValues { predicate(it) }
|
|
||||||
.keys
|
.keys
|
||||||
.toList()
|
.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
|
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)
|
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
|
||||||
.also {
|
.also {
|
||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
|
@ -75,13 +75,13 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||||
return CancelableWork(context, sendRelationWork.id)
|
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(
|
val params = FindReactionEventForUndoTask.Params(
|
||||||
roomId,
|
roomId,
|
||||||
targetEventId,
|
targetEventId,
|
||||||
reaction,
|
reaction
|
||||||
myUserId
|
|
||||||
)
|
)
|
||||||
|
// TODO We should avoid using MatrixCallback internally
|
||||||
val callback = object : MatrixCallback<FindReactionEventForUndoTask.Result> {
|
val callback = object : MatrixCallback<FindReactionEventForUndoTask.Result> {
|
||||||
override fun onSuccess(data: FindReactionEventForUndoTask.Result) {
|
override fun onSuccess(data: FindReactionEventForUndoTask.Result) {
|
||||||
if (data.redactEventId == null) {
|
if (data.redactEventId == null) {
|
||||||
|
@ -89,7 +89,6 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||||
// TODO?
|
// TODO?
|
||||||
}
|
}
|
||||||
data.redactEventId?.let { toRedact ->
|
data.redactEventId?.let { toRedact ->
|
||||||
|
|
||||||
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also {
|
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also {
|
||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
}
|
}
|
||||||
|
@ -99,7 +98,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
findReactionEventForUndoTask
|
return findReactionEventForUndoTask
|
||||||
.configureWith(params) {
|
.configureWith(params) {
|
||||||
this.retryCount = Int.MAX_VALUE
|
this.retryCount = Int.MAX_VALUE
|
||||||
this.callback = callback
|
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.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
|
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
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 im.vector.matrix.android.internal.task.Task
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -29,8 +30,7 @@ internal interface FindReactionEventForUndoTask : Task<FindReactionEventForUndoT
|
||||||
data class Params(
|
data class Params(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String,
|
val eventId: String,
|
||||||
val reaction: String,
|
val reaction: String
|
||||||
val myUserId: String
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Result(
|
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 {
|
override suspend fun execute(params: FindReactionEventForUndoTask.Params): FindReactionEventForUndoTask.Result {
|
||||||
val eventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
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)
|
return FindReactionEventForUndoTask.Result(eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String, userId: String): EventEntity? {
|
private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String): EventEntity? {
|
||||||
val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() ?: return null
|
||||||
if (summary != null) {
|
|
||||||
summary.reactionsSummary.where()
|
val rase = summary.reactionsSummary.where()
|
||||||
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction)
|
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction)
|
||||||
.findFirst()?.let {
|
.findFirst() ?: return null
|
||||||
|
|
||||||
// want to find the event orignated by me!
|
// want to find the event orignated by me!
|
||||||
it.sourceEvents.forEach {
|
return rase.sourceEvents
|
||||||
|
.asSequence()
|
||||||
|
.mapNotNull {
|
||||||
// find source event
|
// find source event
|
||||||
EventEntity.where(realm, it).findFirst()?.let { eventEntity ->
|
EventEntity.where(realm, it).findFirst()
|
||||||
|
}
|
||||||
|
.firstOrNull { eventEntity ->
|
||||||
// is it mine?
|
// is it mine?
|
||||||
if (eventEntity.sender == userId) {
|
eventEntity.sender == userId
|
||||||
return eventEntity
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
|
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
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 im.vector.matrix.android.internal.task.Task
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -30,8 +31,7 @@ internal interface UpdateQuickReactionTask : Task<UpdateQuickReactionTask.Params
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String,
|
val eventId: String,
|
||||||
val reaction: String,
|
val reaction: String,
|
||||||
val oppositeReaction: String,
|
val oppositeReaction: String
|
||||||
val myUserId: String
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Result(
|
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 {
|
override suspend fun execute(params: UpdateQuickReactionTask.Params): UpdateQuickReactionTask.Result {
|
||||||
var res: Pair<String?, List<String>?>? = null
|
var res: Pair<String?, List<String>?>? = null
|
||||||
monarchy.doWithRealm { realm ->
|
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())
|
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
|
// the emoji reaction has been selected, we need to check if we have reacted it or not
|
||||||
val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
?: return Pair(reaction, null)
|
?: return Pair(reaction, null)
|
||||||
|
@ -68,7 +69,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo
|
||||||
val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull {
|
val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull {
|
||||||
// find source event
|
// find source event
|
||||||
val entity = EventEntity.where(realm, it).findFirst()
|
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)
|
return Pair(reaction, toRedact)
|
||||||
} else {
|
} else {
|
||||||
|
@ -77,7 +78,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo
|
||||||
val toRedact = aggregationForReaction.sourceEvents.mapNotNull {
|
val toRedact = aggregationForReaction.sourceEvents.mapNotNull {
|
||||||
// find source event
|
// find source event
|
||||||
val entity = EventEntity.where(realm, it).findFirst()
|
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)
|
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 {
|
private val newSessionListener = object : NewSessionListener {
|
||||||
override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
|
override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
|
||||||
synchronized(unknownSessionsFailure) {
|
synchronized(unknownSessionsFailure) {
|
||||||
val toDecryptAgain = ArrayList<String>()
|
unknownSessionsFailure[sessionId]
|
||||||
unknownSessionsFailure[sessionId]?.let { eventIds ->
|
.orEmpty()
|
||||||
toDecryptAgain.addAll(eventIds)
|
.toList()
|
||||||
}
|
.also {
|
||||||
if (toDecryptAgain.isNotEmpty()) {
|
|
||||||
unknownSessionsFailure[sessionId]?.clear()
|
unknownSessionsFailure[sessionId]?.clear()
|
||||||
toDecryptAgain.forEach {
|
}
|
||||||
|
}.forEach {
|
||||||
requestDecryption(it)
|
requestDecryption(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var executor: ExecutorService? = null
|
private var executor: ExecutorService? = null
|
||||||
|
|
||||||
private val existingRequests = HashSet<String>()
|
// Set of eventIds which are currently decrypting
|
||||||
private val unknownSessionsFailure = HashMap<String, MutableList<String>>()
|
private val existingRequests = mutableSetOf<String>()
|
||||||
|
// sessionId -> list of eventIds
|
||||||
|
private val unknownSessionsFailure = mutableMapOf<String, MutableList<String>>()
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
executor = Executors.newSingleThreadExecutor()
|
executor = Executors.newSingleThreadExecutor()
|
||||||
|
@ -66,26 +66,30 @@ internal class TimelineEventDecryptor(
|
||||||
cryptoService.removeSessionListener(newSessionListener)
|
cryptoService.removeSessionListener(newSessionListener)
|
||||||
executor?.shutdownNow()
|
executor?.shutdownNow()
|
||||||
executor = null
|
executor = null
|
||||||
|
synchronized(unknownSessionsFailure) {
|
||||||
unknownSessionsFailure.clear()
|
unknownSessionsFailure.clear()
|
||||||
|
}
|
||||||
|
synchronized(existingRequests) {
|
||||||
existingRequests.clear()
|
existingRequests.clear()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun requestDecryption(eventId: String) {
|
fun requestDecryption(eventId: String) {
|
||||||
synchronized(existingRequests) {
|
synchronized(unknownSessionsFailure) {
|
||||||
if (existingRequests.contains(eventId)) {
|
for (eventIds in unknownSessionsFailure.values) {
|
||||||
return Unit.also {
|
if (eventId in eventIds) {
|
||||||
Timber.d("Skip Decryption request for event $eventId, already requested")
|
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)
|
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 {
|
executor?.execute {
|
||||||
Realm.getInstance(realmConfiguration).use { realm ->
|
Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
processDecryptRequest(eventId, realm)
|
processDecryptRequest(eventId, realm)
|
||||||
|
@ -107,7 +111,7 @@ internal class TimelineEventDecryptor(
|
||||||
eventEntity.setDecryptionResult(result)
|
eventEntity.setDecryptionResult(result)
|
||||||
}
|
}
|
||||||
} catch (e: MXCryptoError) {
|
} 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) {
|
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
|
// Keep track of unknown sessions to automatically try to decrypt on new session
|
||||||
realm.executeTransaction {
|
realm.executeTransaction {
|
||||||
|
@ -116,10 +120,7 @@ internal class TimelineEventDecryptor(
|
||||||
event.content?.toModel<EncryptedEventContent>()?.let { content ->
|
event.content?.toModel<EncryptedEventContent>()?.let { content ->
|
||||||
content.sessionId?.let { sessionId ->
|
content.sessionId?.let { sessionId ->
|
||||||
synchronized(unknownSessionsFailure) {
|
synchronized(unknownSessionsFailure) {
|
||||||
val list = unknownSessionsFailure[sessionId]
|
val list = unknownSessionsFailure.getOrPut(sessionId) { ArrayList() }
|
||||||
?: ArrayList<String>().also {
|
|
||||||
unknownSessionsFailure[sessionId] = it
|
|
||||||
}
|
|
||||||
list.add(eventId)
|
list.add(eventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -461,9 +461,9 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
|
||||||
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
|
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
|
||||||
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
val outputStream = ByteArrayOutputStream()
|
||||||
val cipherOutputStream = CipherOutputStream(outputStream, inputCipher)
|
CipherOutputStream(outputStream, inputCipher).use {
|
||||||
cipherOutputStream.write(secret)
|
it.write(secret)
|
||||||
cipherOutputStream.close()
|
}
|
||||||
|
|
||||||
return outputStream.toByteArray()
|
return outputStream.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.sync
|
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.crypto.MXCryptoError
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
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.EventType
|
||||||
|
@ -41,9 +40,9 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
|
||||||
initialSyncProgressService?.reportProgress(((index / total.toFloat()) * 100).toInt())
|
initialSyncProgressService?.reportProgress(((index / total.toFloat()) * 100).toInt())
|
||||||
// Decrypt event if necessary
|
// Decrypt event if necessary
|
||||||
decryptEvent(event, null)
|
decryptEvent(event, null)
|
||||||
if (TextUtils.equals(event.getClearType(), EventType.MESSAGE)
|
if (event.getClearType() == EventType.MESSAGE
|
||||||
&& event.getClearContent()?.toModel<MessageContent>()?.type == "m.bad.encrypted") {
|
&& 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 {
|
} else {
|
||||||
sasVerificationService.onToDeviceEvent(event)
|
sasVerificationService.onToDeviceEvent(event)
|
||||||
cryptoService.onToDeviceEvent(event)
|
cryptoService.onToDeviceEvent(event)
|
||||||
|
|
|
@ -25,7 +25,7 @@ import io.realm.Realm
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
// the receipts dictionnaries
|
// the receipts dictionaries
|
||||||
// key : $EventId
|
// key : $EventId
|
||||||
// value : dict key $UserId
|
// value : dict key $UserId
|
||||||
// value dict key ts
|
// 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 im.vector.matrix.android.internal.task.configureWith
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
import java.util.*
|
import java.util.Timer
|
||||||
|
import java.util.TimerTask
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can execute periodic sync task.
|
* Can execute periodic sync task.
|
||||||
|
|
|
@ -31,18 +31,10 @@ internal class DirectChatsHelper @Inject constructor(@SessionDatabase
|
||||||
*/
|
*/
|
||||||
fun getLocalUserAccount(filterRoomId: String? = null): MutableMap<String, MutableList<String>> {
|
fun getLocalUserAccount(filterRoomId: String? = null): MutableMap<String, MutableList<String>> {
|
||||||
return Realm.getInstance(realmConfiguration).use { realm ->
|
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
val currentDirectRooms = RoomSummaryEntity.getDirectRooms(realm)
|
RoomSummaryEntity.getDirectRooms(realm)
|
||||||
val directChatsMap = mutableMapOf<String, MutableList<String>>()
|
.asSequence()
|
||||||
for (directRoom in currentDirectRooms) {
|
.filter { it.roomId != filterRoomId && it.directUserId != null }
|
||||||
if (directRoom.roomId == filterRoomId) continue
|
.groupByTo(mutableMapOf(), { it.directUserId!! }, { it.roomId })
|
||||||
val directUserId = directRoom.directUserId ?: continue
|
|
||||||
directChatsMap
|
|
||||||
.getOrPut(directUserId, { arrayListOf() })
|
|
||||||
.apply {
|
|
||||||
add(directRoom.roomId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
directChatsMap
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.task
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
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,
|
internal fun <PARAMS, RESULT> Task<PARAMS, RESULT>.configureWith(params: PARAMS,
|
||||||
init: (ConfigurableTask.Builder<PARAMS, RESULT>.() -> Unit) = {}
|
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 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 sSecretKeyAndVersion: SecretKeyAndVersion? = null
|
||||||
private var sPrng: SecureRandom? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the unique SecureRandom instance shared for all local storage encryption operations.
|
* Returns the unique SecureRandom instance shared for all local storage encryption operations.
|
||||||
*/
|
*/
|
||||||
private val prng: SecureRandom
|
private val prng: SecureRandom by lazy(LazyThreadSafetyMode.NONE) { SecureRandom() }
|
||||||
get() {
|
|
||||||
if (sPrng == null) {
|
|
||||||
sPrng = SecureRandom()
|
|
||||||
}
|
|
||||||
|
|
||||||
return sPrng!!
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a GZIPOutputStream instance
|
* Create a GZIPOutputStream instance
|
||||||
|
|
|
@ -24,12 +24,9 @@ import java.security.MessageDigest
|
||||||
fun String.md5() = try {
|
fun String.md5() = try {
|
||||||
val digest = MessageDigest.getInstance("md5")
|
val digest = MessageDigest.getInstance("md5")
|
||||||
digest.update(toByteArray())
|
digest.update(toByteArray())
|
||||||
val bytes = digest.digest()
|
digest.digest()
|
||||||
val sb = StringBuilder()
|
.joinToString("") { String.format("%02X", it) }
|
||||||
for (i in bytes.indices) {
|
.toLowerCase()
|
||||||
sb.append(String.format("%02X", bytes[i]))
|
|
||||||
}
|
|
||||||
sb.toString().toLowerCase()
|
|
||||||
} catch (exc: Exception) {
|
} catch (exc: Exception) {
|
||||||
// Should not happen, but just in case
|
// Should not happen, but just in case
|
||||||
hashCode().toString()
|
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.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
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.Cancelable
|
||||||
|
import im.vector.matrix.android.api.util.Optional
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
@ -172,7 +173,6 @@ class PushrulesConditionTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockRoomService() : RoomService {
|
class MockRoomService() : RoomService {
|
||||||
|
|
||||||
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
|
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
|
||||||
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
|
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>> {
|
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
|
||||||
return MutableLiveData()
|
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 {
|
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? {
|
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
|
||||||
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
|
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 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.
|
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,7 +262,7 @@ class PushrulesConditionTest {
|
||||||
return _numberOfJoinedMembers
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
TODO("not implemented") // To change body of created functions use File | Settings | File Templates.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -281,7 +281,7 @@ dependencies {
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
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 'me.gujun.android:span:1.7'
|
||||||
implementation "ru.noties.markwon:core:$markwon_version"
|
implementation "ru.noties.markwon:core:$markwon_version"
|
||||||
implementation "ru.noties.markwon:html:$markwon_version"
|
implementation "ru.noties.markwon:html:$markwon_version"
|
||||||
|
|
|
@ -59,7 +59,7 @@ abstract class DebugMaterialThemeActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.vector_home, menu)
|
menuInflater.inflate(R.menu.home, menu)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ package im.vector.riotx.gplay.push.fcm
|
||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.text.TextUtils
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
|
@ -214,10 +213,10 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (notifiableEvent is NotifiableMessageEvent) {
|
if (notifiableEvent is NotifiableMessageEvent) {
|
||||||
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
|
if (notifiableEvent.senderName.isNullOrEmpty()) {
|
||||||
notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
|
notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
|
||||||
}
|
}
|
||||||
if (TextUtils.isEmpty(notifiableEvent.roomName)) {
|
if (notifiableEvent.roomName.isNullOrEmpty()) {
|
||||||
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
|
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.RoomDetailFragment
|
||||||
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
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.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.filtered.FilteredRoomsActivity
|
||||||
import im.vector.riotx.features.home.room.list.RoomListFragment
|
import im.vector.riotx.features.home.room.list.RoomListFragment
|
||||||
import im.vector.riotx.features.invite.VectorInviteView
|
import im.vector.riotx.features.invite.VectorInviteView
|
||||||
|
@ -104,12 +106,10 @@ interface ScreenComponent {
|
||||||
|
|
||||||
fun inject(messageActionsBottomSheet: MessageActionsBottomSheet)
|
fun inject(messageActionsBottomSheet: MessageActionsBottomSheet)
|
||||||
|
|
||||||
fun inject(viewReactionBottomSheet: ViewReactionBottomSheet)
|
fun inject(viewReactionsBottomSheet: ViewReactionsBottomSheet)
|
||||||
|
|
||||||
fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
|
fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
|
||||||
|
|
||||||
fun inject(messageMenuFragment: MessageMenuFragment)
|
|
||||||
|
|
||||||
fun inject(vectorSettingsActivity: VectorSettingsActivity)
|
fun inject(vectorSettingsActivity: VectorSettingsActivity)
|
||||||
|
|
||||||
fun inject(createRoomFragment: CreateRoomFragment)
|
fun inject(createRoomFragment: CreateRoomFragment)
|
||||||
|
@ -136,8 +136,6 @@ interface ScreenComponent {
|
||||||
|
|
||||||
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
|
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
|
||||||
|
|
||||||
fun inject(quickReactionFragment: QuickReactionFragment)
|
|
||||||
|
|
||||||
fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity)
|
fun inject(emojiReactionPickerActivity: EmojiReactionPickerActivity)
|
||||||
|
|
||||||
fun inject(loginActivity: LoginActivity)
|
fun inject(loginActivity: LoginActivity)
|
||||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.riotx.core.dialogs
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextUtils
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
@ -45,11 +44,11 @@ class ExportKeysDialog {
|
||||||
val textWatcher = object : SimpleTextWatcher() {
|
val textWatcher = object : SimpleTextWatcher() {
|
||||||
override fun afterTextChanged(s: Editable) {
|
override fun afterTextChanged(s: Editable) {
|
||||||
when {
|
when {
|
||||||
TextUtils.isEmpty(passPhrase1EditText.text) -> {
|
passPhrase1EditText.text.isNullOrEmpty() -> {
|
||||||
exportButton.isEnabled = false
|
exportButton.isEnabled = false
|
||||||
passPhrase2Til.error = null
|
passPhrase2Til.error = null
|
||||||
}
|
}
|
||||||
TextUtils.equals(passPhrase1EditText.text, passPhrase2EditText.text) -> {
|
passPhrase1EditText.text == passPhrase2EditText.text -> {
|
||||||
exportButton.isEnabled = true
|
exportButton.isEnabled = true
|
||||||
passPhrase2Til.error = null
|
passPhrase2Til.error = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.TextUtils
|
|
||||||
import androidx.core.util.PatternsCompat.WEB_URL
|
import androidx.core.util.PatternsCompat.WEB_URL
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inspired from Riot code: RoomMediaMessage.java
|
* 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
|
// chrome adds many items when sharing an web page link
|
||||||
// so, test first the type
|
// 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)
|
var message: String? = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
|
?: intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
|
||||||
if (null == message) {
|
|
||||||
val sequence = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
|
|
||||||
if (null != sequence) {
|
|
||||||
message = sequence.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(subject)) {
|
if (!subject.isNullOrEmpty()) {
|
||||||
if (TextUtils.isEmpty(message)) {
|
if (message.isNullOrEmpty()) {
|
||||||
message = subject
|
message = subject
|
||||||
} else if (WEB_URL.matcher(message!!).matches()) {
|
} else if (WEB_URL.matcher(message).matches()) {
|
||||||
message = subject + "\n" + message
|
message = subject + "\n" + message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(message)) {
|
if (!message.isNullOrEmpty()) {
|
||||||
externalIntentDataList.add(ExternalIntentData.IntentDataText(message!!, null, intent.type))
|
externalIntentDataList.add(ExternalIntentData.IntentDataText(message, null, intent.type))
|
||||||
return externalIntentDataList
|
return externalIntentDataList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var clipData: ClipData? = null
|
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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
clipData = intent.clipData
|
clipData = intent.clipData
|
||||||
|
@ -106,41 +98,26 @@ fun analyseIntent(intent: Intent): List<ExternalIntentData> {
|
||||||
if (null != clipData) {
|
if (null != clipData) {
|
||||||
if (null != clipData.description) {
|
if (null != clipData.description) {
|
||||||
if (0 != clipData.description.mimeTypeCount) {
|
if (0 != clipData.description.mimeTypeCount) {
|
||||||
mimetypes = ArrayList()
|
mimeTypes = with(clipData.description) {
|
||||||
|
List(mimeTypeCount) { getMimeType(it) }
|
||||||
for (i in 0 until clipData.description.mimeTypeCount) {
|
|
||||||
mimetypes.add(clipData.description.getMimeType(i))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the filter is "accept anything" the mimetype does not make sense
|
// if the filter is "accept anything" the mimetype does not make sense
|
||||||
if (1 == mimetypes.size) {
|
if (1 == mimeTypes.size) {
|
||||||
if (mimetypes[0].endsWith("/*")) {
|
if (mimeTypes[0].endsWith("/*")) {
|
||||||
mimetypes = null
|
mimeTypes = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val count = clipData.itemCount
|
for (i in 0 until clipData.itemCount) {
|
||||||
|
|
||||||
for (i in 0 until count) {
|
|
||||||
val item = clipData.getItemAt(i)
|
val item = clipData.getItemAt(i)
|
||||||
var mimetype: String? = null
|
val mimeType = mimeTypes?.getOrElse(i) { mimeTypes[0] }
|
||||||
|
|
||||||
if (null != mimetypes) {
|
|
||||||
if (i < mimetypes.size) {
|
|
||||||
mimetype = mimetypes[i]
|
|
||||||
} else {
|
|
||||||
mimetype = mimetypes[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// uris list is not a valid mimetype
|
// uris list is not a valid mimetype
|
||||||
if (TextUtils.equals(mimetype, ClipDescription.MIMETYPE_TEXT_URILIST)) {
|
.takeUnless { it == ClipDescription.MIMETYPE_TEXT_URILIST }
|
||||||
mimetype = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimetype))
|
externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimeType))
|
||||||
}
|
}
|
||||||
} else if (null != intent.data) {
|
} else if (null != intent.data) {
|
||||||
externalIntentDataList.add(ExternalIntentData.IntentDataUri(intent.data!!))
|
externalIntentDataList.add(ExternalIntentData.IntentDataUri(intent.data!!))
|
||||||
|
|
|
@ -13,18 +13,23 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
import com.airbnb.mvrx.MvRx
|
import com.airbnb.mvrx.MvRx
|
||||||
import com.airbnb.mvrx.MvRxView
|
import com.airbnb.mvrx.MvRxView
|
||||||
import com.airbnb.mvrx.MvRxViewModelStore
|
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 com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import im.vector.riotx.core.di.DaggerScreenComponent
|
import im.vector.riotx.core.di.DaggerScreenComponent
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
import im.vector.riotx.core.utils.DimensionConverter
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,10 +42,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
|
||||||
private lateinit var screenComponent: ScreenComponent
|
private lateinit var screenComponent: ScreenComponent
|
||||||
final override val mvrxViewId: String by lazy { mvrxPersistedViewId }
|
final override val mvrxViewId: String by lazy { mvrxPersistedViewId }
|
||||||
|
|
||||||
|
private var bottomSheetBehavior: BottomSheetBehavior<FrameLayout>? = null
|
||||||
|
|
||||||
val vectorBaseActivity: VectorBaseActivity by lazy {
|
val vectorBaseActivity: VectorBaseActivity by lazy {
|
||||||
activity as VectorBaseActivity
|
activity as VectorBaseActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open val showExpanded = false
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
|
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
|
@ -57,6 +66,17 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
|
||||||
super.onCreate(savedInstanceState)
|
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) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
mvrxViewModelStore.saveViewModels(outState)
|
mvrxViewModelStore.saveViewModels(outState)
|
||||||
|
@ -70,6 +90,14 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
|
||||||
postInvalidate()
|
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) {
|
protected fun setArguments(args: Parcelable? = null) {
|
||||||
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
|
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
|
||||||
}
|
}
|
|
@ -17,7 +17,6 @@
|
||||||
package im.vector.riotx.core.preference
|
package im.vector.riotx.core.preference
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.RadioGroup
|
import android.widget.RadioGroup
|
||||||
|
@ -84,7 +83,7 @@ class BingRulePreference : VectorPreference {
|
||||||
val ruleStatusIndex: Int
|
val ruleStatusIndex: Int
|
||||||
get() {
|
get() {
|
||||||
if (null != rule) {
|
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()) {
|
if (rule!!.shouldNotNotify()) {
|
||||||
return if (rule!!.isEnabled) {
|
return if (rule!!.isEnabled) {
|
||||||
NOTIFICATION_OFF_INDEX
|
NOTIFICATION_OFF_INDEX
|
||||||
|
@ -143,7 +142,7 @@ class BingRulePreference : VectorPreference {
|
||||||
if (null != this.rule && index != ruleStatusIndex) {
|
if (null != this.rule && index != ruleStatusIndex) {
|
||||||
rule = BingRule(this.rule!!)
|
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) {
|
when (index) {
|
||||||
NOTIFICATION_OFF_INDEX -> {
|
NOTIFICATION_OFF_INDEX -> {
|
||||||
rule.isEnabled = true
|
rule.isEnabled = true
|
||||||
|
@ -164,8 +163,8 @@ class BingRulePreference : VectorPreference {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NOTIFICATION_OFF_INDEX == index) {
|
if (NOTIFICATION_OFF_INDEX == index) {
|
||||||
if (TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE)
|
if (this.rule!!.kind == BingRule.KIND_UNDERRIDE
|
||||||
|| TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
|
|| rule.ruleId == BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS) {
|
||||||
rule.setNotify(false)
|
rule.setNotify(false)
|
||||||
} else {
|
} else {
|
||||||
rule.isEnabled = false
|
rule.isEnabled = false
|
||||||
|
@ -173,11 +172,11 @@ class BingRulePreference : VectorPreference {
|
||||||
} else {
|
} else {
|
||||||
rule.isEnabled = true
|
rule.isEnabled = true
|
||||||
rule.setNotify(true)
|
rule.setNotify(true)
|
||||||
rule.setHighlight(!TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE)
|
rule.setHighlight(this.rule!!.kind != BingRule.KIND_UNDERRIDE
|
||||||
&& !TextUtils.equals(rule.ruleId, BingRule.RULE_ID_INVITE_ME)
|
&& rule.ruleId != BingRule.RULE_ID_INVITE_ME
|
||||||
&& NOTIFICATION_NOISY_INDEX == index)
|
&& NOTIFICATION_NOISY_INDEX == index)
|
||||||
if (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
|
BingRule.ACTION_VALUE_RING
|
||||||
} else {
|
} else {
|
||||||
BingRule.ACTION_VALUE_DEFAULT
|
BingRule.ACTION_VALUE_DEFAULT
|
||||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.riotx.core.resources
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.TextUtils
|
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import im.vector.riotx.core.utils.getFileExtension
|
import im.vector.riotx.core.utils.getFileExtension
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -73,7 +72,7 @@ fun openResource(context: Context, uri: Uri, providedMimetype: String?): Resourc
|
||||||
var mimetype = providedMimetype
|
var mimetype = providedMimetype
|
||||||
try {
|
try {
|
||||||
// if the mime type is not provided, try to find it out
|
// if the mime type is not provided, try to find it out
|
||||||
if (TextUtils.isEmpty(mimetype)) {
|
if (mimetype.isNullOrEmpty()) {
|
||||||
mimetype = context.contentResolver.getType(uri)
|
mimetype = context.contentResolver.getType(uri)
|
||||||
|
|
||||||
// try to find the mimetype from the filename
|
// try to find the mimetype from the filename
|
||||||
|
|
|
@ -20,7 +20,6 @@ import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
import android.text.TextUtils
|
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.text.style.ClickableSpan
|
import android.text.style.ClickableSpan
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
@ -168,7 +167,7 @@ class NotificationAreaView @JvmOverloads constructor(
|
||||||
} else {
|
} else {
|
||||||
imageView.setImageResource(R.drawable.scrolldown)
|
imageView.setImageResource(R.drawable.scrolldown)
|
||||||
messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
|
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)
|
messageView.text = SpannableString(state.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ fun openUrlInExternalBrowser(context: Context, uri: Uri?) {
|
||||||
uri?.let {
|
uri?.let {
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, it).apply {
|
val browserIntent = Intent(Intent.ACTION_VIEW, it).apply {
|
||||||
putExtra(Browser.EXTRA_APPLICATION_ID, context.packageName)
|
putExtra(Browser.EXTRA_APPLICATION_ID, context.packageName)
|
||||||
|
putExtra(Browser.EXTRA_CREATE_NEW_TAB, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
package im.vector.riotx.core.utils
|
package im.vector.riotx.core.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.TextUtils
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@ -60,7 +59,7 @@ private fun logAction(file: File): Boolean {
|
||||||
if (file.isDirectory) {
|
if (file.isDirectory) {
|
||||||
Timber.v(file.toString())
|
Timber.v(file.toString())
|
||||||
} else {
|
} else {
|
||||||
Timber.v(file.toString() + " " + file.length() + " bytes")
|
Timber.v("$file ${file.length()} bytes")
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -96,26 +95,19 @@ private fun recursiveActionOnFile(file: File, action: ActionOnFile): Boolean {
|
||||||
fun getFileExtension(fileUri: String): String? {
|
fun getFileExtension(fileUri: String): String? {
|
||||||
var reducedStr = fileUri
|
var reducedStr = fileUri
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(reducedStr)) {
|
if (reducedStr.isNotEmpty()) {
|
||||||
// Remove fragment
|
// Remove fragment
|
||||||
val fragment = fileUri.lastIndexOf('#')
|
reducedStr = reducedStr.substringBeforeLast('#')
|
||||||
if (fragment > 0) {
|
|
||||||
reducedStr = fileUri.substring(0, fragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove query
|
// Remove query
|
||||||
val query = reducedStr.lastIndexOf('?')
|
reducedStr = reducedStr.substringBeforeLast('?')
|
||||||
if (query > 0) {
|
|
||||||
reducedStr = reducedStr.substring(0, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove path
|
// Remove path
|
||||||
val filenamePos = reducedStr.lastIndexOf('/')
|
val filename = reducedStr.substringAfterLast('/')
|
||||||
val filename = if (0 <= filenamePos) reducedStr.substring(filenamePos + 1) else reducedStr
|
|
||||||
|
|
||||||
// Contrary to method MimeTypeMap.getFileExtensionFromUrl, we do not check the pattern
|
// Contrary to method MimeTypeMap.getFileExtensionFromUrl, we do not check the pattern
|
||||||
// See https://stackoverflow.com/questions/14320527/android-should-i-use-mimetypemap-getfileextensionfromurl-bugs
|
// See https://stackoverflow.com/questions/14320527/android-should-i-use-mimetypemap-getfileextensionfromurl-bugs
|
||||||
if (!filename.isEmpty()) {
|
if (filename.isNotEmpty()) {
|
||||||
val dotPos = filename.lastIndexOf('.')
|
val dotPos = filename.lastIndexOf('.')
|
||||||
if (0 <= dotPos) {
|
if (0 <= dotPos) {
|
||||||
val ext = filename.substring(dotPos + 1)
|
val ext = filename.substring(dotPos + 1)
|
||||||
|
@ -134,15 +126,11 @@ fun getFileExtension(fileUri: String): String? {
|
||||||
* Size
|
* Size
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
|
||||||
fun getSizeOfFiles(context: Context, root: File): Int {
|
fun getSizeOfFiles(root: File): Int {
|
||||||
Timber.v("Get size of " + root.absolutePath)
|
return root.walkTopDown()
|
||||||
return if (root.isDirectory) {
|
.onEnter {
|
||||||
root.list()
|
Timber.v("Get size of ${it.absolutePath}")
|
||||||
.map {
|
true
|
||||||
getSizeOfFiles(context, File(root, it))
|
|
||||||
}
|
|
||||||
.fold(0, { acc, other -> acc + other })
|
|
||||||
} else {
|
|
||||||
root.length().toInt()
|
|
||||||
}
|
}
|
||||||
|
.sumBy { it.length().toInt() }
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,6 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
private const val LOG_TAG = "PermissionUtils"
|
private const val LOG_TAG = "PermissionUtils"
|
||||||
|
|
||||||
|
@ -74,7 +73,7 @@ const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
|
||||||
*/
|
*/
|
||||||
fun logPermissionStatuses(context: Context) {
|
fun logPermissionStatuses(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
val permissions = Arrays.asList(
|
val permissions = listOf(
|
||||||
Manifest.permission.CAMERA,
|
Manifest.permission.CAMERA,
|
||||||
Manifest.permission.RECORD_AUDIO,
|
Manifest.permission.RECORD_AUDIO,
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||||
|
@ -213,7 +212,7 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
||||||
.setMessage(rationaleMessage)
|
.setMessage(rationaleMessage)
|
||||||
.setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() }
|
.setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() }
|
||||||
.setPositiveButton(R.string.ok) { _, _ ->
|
.setPositiveButton(R.string.ok) { _, _ ->
|
||||||
if (!permissionsListToBeGranted.isEmpty()) {
|
if (permissionsListToBeGranted.isNotEmpty()) {
|
||||||
fragment?.requestPermissions(permissionsListToBeGranted.toTypedArray(), requestCode)
|
fragment?.requestPermissions(permissionsListToBeGranted.toTypedArray(), requestCode)
|
||||||
?: run {
|
?: run {
|
||||||
ActivityCompat.requestPermissions(activity, permissionsListToBeGranted.toTypedArray(), requestCode)
|
ActivityCompat.requestPermissions(activity, permissionsListToBeGranted.toTypedArray(), requestCode)
|
||||||
|
|
|
@ -24,9 +24,9 @@ import java.util.*
|
||||||
object TextUtils {
|
object TextUtils {
|
||||||
|
|
||||||
private val suffixes = TreeMap<Int, String>().also {
|
private val suffixes = TreeMap<Int, String>().also {
|
||||||
it.put(1000, "k")
|
it[1000] = "k"
|
||||||
it.put(1000000, "M")
|
it[1000000] = "M"
|
||||||
it.put(1000000000, "G")
|
it[1000000000] = "G"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatCountToShortDecimal(value: Int): String {
|
fun formatCountToShortDecimal(value: Int): String {
|
||||||
|
|
|
@ -17,7 +17,6 @@ package im.vector.riotx.features.crypto.keysbackup.setup
|
||||||
|
|
||||||
import android.os.AsyncTask
|
import android.os.AsyncTask
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
|
@ -122,7 +121,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
|
||||||
})
|
})
|
||||||
|
|
||||||
viewModel.passphrase.observe(this, Observer<String> { newValue ->
|
viewModel.passphrase.observe(this, Observer<String> { newValue ->
|
||||||
if (TextUtils.isEmpty(newValue)) {
|
if (newValue.isEmpty()) {
|
||||||
viewModel.passwordStrength.value = null
|
viewModel.passwordStrength.value = null
|
||||||
} else {
|
} else {
|
||||||
AsyncTask.execute {
|
AsyncTask.execute {
|
||||||
|
@ -172,7 +171,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
|
||||||
@OnClick(R.id.keys_backup_setup_step2_button)
|
@OnClick(R.id.keys_backup_setup_step2_button)
|
||||||
fun doNext() {
|
fun doNext() {
|
||||||
when {
|
when {
|
||||||
TextUtils.isEmpty(viewModel.passphrase.value) -> {
|
viewModel.passphrase.value.isNullOrEmpty() -> {
|
||||||
viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message)
|
viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message)
|
||||||
}
|
}
|
||||||
viewModel.passphrase.value != viewModel.confirmPassphrase.value -> {
|
viewModel.passphrase.value != viewModel.confirmPassphrase.value -> {
|
||||||
|
@ -192,7 +191,7 @@ class KeysBackupSetupStep2Fragment : VectorBaseFragment() {
|
||||||
@OnClick(R.id.keys_backup_setup_step2_skip_button)
|
@OnClick(R.id.keys_backup_setup_step2_skip_button)
|
||||||
fun skipPassphrase() {
|
fun skipPassphrase() {
|
||||||
when {
|
when {
|
||||||
TextUtils.isEmpty(viewModel.passphrase.value) -> {
|
viewModel.passphrase.value.isNullOrEmpty() -> {
|
||||||
// Generate a recovery key for the user
|
// Generate a recovery key for the user
|
||||||
viewModel.megolmBackupCreationInfo = null
|
viewModel.megolmBackupCreationInfo = null
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
package im.vector.riotx.features.crypto.keysrequest
|
package im.vector.riotx.features.crypto.keysrequest
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.TextUtils
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
|
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 timber.log.Timber
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
@ -100,7 +100,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||||
alertsToRequests[mappingKey] = ArrayList<IncomingRoomKeyRequest>().apply { this.add(request) }
|
alertsToRequests[mappingKey] = ArrayList<IncomingRoomKeyRequest>().apply { this.add(request) }
|
||||||
|
|
||||||
// Add a notification for every incoming 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>) {
|
override fun onSuccess(data: MXUsersDevicesMap<MXDeviceInfo>) {
|
||||||
val deviceInfo = data.getObject(userId, deviceId)
|
val deviceInfo = data.getObject(userId, deviceId)
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||||
wasNewDevice: Boolean,
|
wasNewDevice: Boolean,
|
||||||
deviceInfo: MXDeviceInfo?,
|
deviceInfo: MXDeviceInfo?,
|
||||||
moreInfo: DeviceInfo? = null) {
|
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?
|
val dialogText: String?
|
||||||
|
|
||||||
if (moreInfo != null) {
|
if (moreInfo != null) {
|
||||||
|
@ -244,12 +244,12 @@ class KeyRequestHandler @Inject constructor(private val context: Context)
|
||||||
val deviceId = request.deviceId
|
val deviceId = request.deviceId
|
||||||
val requestId = request.requestId
|
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")
|
Timber.e("## handleKeyRequestCancellation() : invalid parameters")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val alertMgrUniqueKey = alertManagerId(deviceId!!, userId!!)
|
val alertMgrUniqueKey = alertManagerId(deviceId, userId)
|
||||||
alertsToRequests[alertMgrUniqueKey]?.removeAll {
|
alertsToRequests[alertMgrUniqueKey]?.removeAll {
|
||||||
it.deviceId == request.deviceId
|
it.deviceId == request.deviceId
|
||||||
&& it.userId == request.userId
|
&& it.userId == request.userId
|
||||||
|
|
|
@ -179,7 +179,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
|
|
|
@ -30,9 +30,9 @@ sealed class RoomDetailActions {
|
||||||
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
|
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
|
||||||
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
|
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
|
||||||
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : 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 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 UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
|
||||||
data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions()
|
data class NavigateToEvent(val eventId: String, val highlight: Boolean) : RoomDetailActions()
|
||||||
data class SetReadMarkerAction(val eventId: String) : RoomDetailActions()
|
data class SetReadMarkerAction(val eventId: String) : RoomDetailActions()
|
||||||
|
@ -49,6 +49,9 @@ sealed class RoomDetailActions {
|
||||||
|
|
||||||
data class ResendMessage(val eventId: String) : RoomDetailActions()
|
data class ResendMessage(val eventId: String) : RoomDetailActions()
|
||||||
data class RemoveFailedEcho(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 ClearSendQueue : RoomDetailActions()
|
||||||
object ResendAll : RoomDetailActions()
|
object ResendAll : RoomDetailActions()
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity.RESULT_OK
|
import android.app.Activity.RESULT_OK
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -50,6 +51,7 @@ import com.airbnb.mvrx.*
|
||||||
import com.github.piasy.biv.BigImageViewer
|
import com.github.piasy.biv.BigImageViewer
|
||||||
import com.github.piasy.biv.loader.ImageLoader
|
import com.github.piasy.biv.loader.ImageLoader
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.otaliastudios.autocomplete.Autocomplete
|
import com.otaliastudios.autocomplete.Autocomplete
|
||||||
import com.otaliastudios.autocomplete.AutocompleteCallback
|
import com.otaliastudios.autocomplete.AutocompleteCallback
|
||||||
import com.otaliastudios.autocomplete.CharPolicy
|
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.matrix.android.api.session.user.model.User
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
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.epoxy.LayoutManagerStateRestorer
|
||||||
import im.vector.riotx.core.error.ErrorFormatter
|
import im.vector.riotx.core.error.ErrorFormatter
|
||||||
import im.vector.riotx.core.extensions.hideKeyboard
|
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.composer.TextComposerViewState
|
||||||
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
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.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.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.EventHtmlRenderer
|
||||||
import im.vector.riotx.features.html.PillImageSpan
|
import im.vector.riotx.features.html.PillImageSpan
|
||||||
import im.vector.riotx.features.invite.VectorInviteView
|
import im.vector.riotx.features.invite.VectorInviteView
|
||||||
|
@ -274,6 +281,10 @@ class RoomDetailFragment :
|
||||||
syncStateView.render(syncState)
|
syncStateView.render(syncState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
roomDetailViewModel.requestLiveData.observeEvent(this) {
|
||||||
|
displayRoomDetailActionResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
when (val sharedData = roomDetailArgs.sharedData) {
|
when (val sharedData = roomDetailArgs.sharedData) {
|
||||||
is SharedData.Text -> roomDetailViewModel.process(RoomDetailActions.SendMessage(sharedData.text, false))
|
is SharedData.Text -> roomDetailViewModel.process(RoomDetailActions.SendMessage(sharedData.text, false))
|
||||||
|
@ -281,6 +292,7 @@ class RoomDetailFragment :
|
||||||
null -> Timber.v("No share data to process")
|
null -> Timber.v("No share data to process")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
@ -438,7 +450,7 @@ class RoomDetailFragment :
|
||||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
||||||
?: return
|
?: return
|
||||||
// TODO check if already reacted with that?
|
// 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) {
|
private fun displayCommandError(message: String) {
|
||||||
AlertDialog.Builder(activity!!)
|
AlertDialog.Builder(requireActivity())
|
||||||
.setTitle(R.string.command_error)
|
.setTitle(R.string.command_error)
|
||||||
.setMessage(message)
|
.setMessage(message)
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.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 ************************************************************
|
// TimelineEventController.Callback ************************************************************
|
||||||
|
|
||||||
override fun onUrlClicked(url: String): Boolean {
|
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 {
|
override fun navToRoom(roomId: String, eventId: String?): Boolean {
|
||||||
// Same room?
|
// Same room?
|
||||||
if (roomId == roomDetailArgs.roomId) {
|
if (roomId == roomDetailArgs.roomId) {
|
||||||
|
@ -760,6 +836,14 @@ class RoomDetailFragment :
|
||||||
return false
|
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 {
|
override fun onUrlLongClicked(url: String): Boolean {
|
||||||
|
@ -872,7 +956,7 @@ class RoomDetailFragment :
|
||||||
override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
|
override fun onClickOnReactionPill(informationData: MessageInformationData, reaction: String, on: Boolean) {
|
||||||
if (on) {
|
if (on) {
|
||||||
// we should test the current real state of reaction on this event
|
// 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 {
|
} else {
|
||||||
// I need to redact a reaction
|
// I need to redact a reaction
|
||||||
roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction))
|
roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction))
|
||||||
|
@ -880,7 +964,7 @@ class RoomDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) {
|
override fun onLongClickOnReactionPill(informationData: MessageInformationData, reaction: String) {
|
||||||
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
|
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
|
||||||
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -933,7 +1017,7 @@ class RoomDetailFragment :
|
||||||
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
|
startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE)
|
||||||
}
|
}
|
||||||
is SimpleAction.ViewReactions -> {
|
is SimpleAction.ViewReactions -> {
|
||||||
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
|
ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData)
|
||||||
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
|
||||||
}
|
}
|
||||||
is SimpleAction.Copy -> {
|
is SimpleAction.Copy -> {
|
||||||
|
@ -1022,6 +1106,15 @@ class RoomDetailFragment :
|
||||||
is SimpleAction.Remove -> {
|
is SimpleAction.Remove -> {
|
||||||
roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId))
|
roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId))
|
||||||
}
|
}
|
||||||
|
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 -> {
|
else -> {
|
||||||
Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show()
|
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
|
package im.vector.riotx.features.home.room.detail
|
||||||
|
|
||||||
import android.text.TextUtils
|
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.*
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
|
||||||
import com.airbnb.mvrx.Success
|
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
|
||||||
import com.jakewharton.rxrelay2.BehaviorRelay
|
import com.jakewharton.rxrelay2.BehaviorRelay
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
@ -92,6 +88,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
|
|
||||||
private var timeline = room.createTimeline(eventId, timelineSettings)
|
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
|
// Slot to keep a pending action during permission request
|
||||||
var pendingAction: RoomDetailActions? = null
|
var pendingAction: RoomDetailActions? = null
|
||||||
|
|
||||||
|
@ -150,6 +151,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
is RoomDetailActions.ResendAll -> handleResendAll()
|
is RoomDetailActions.ResendAll -> handleResendAll()
|
||||||
is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action)
|
is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action)
|
||||||
is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead()
|
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 document = parser.parse(finalText)
|
||||||
val renderer = HtmlRenderer.builder().build()
|
val renderer = HtmlRenderer.builder().build()
|
||||||
val htmlText = renderer.render(document)
|
val htmlText = renderer.render(document)
|
||||||
if (TextUtils.equals(finalText, htmlText)) {
|
if (finalText == htmlText) {
|
||||||
room.sendTextMessage(finalText)
|
room.sendTextMessage(finalText)
|
||||||
} else {
|
} else {
|
||||||
room.sendFormattedTextMessage(finalText, htmlText)
|
room.sendFormattedTextMessage(finalText, htmlText)
|
||||||
|
@ -396,19 +398,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
|
|
||||||
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
|
private fun legacyRiotQuoteText(quotedText: String?, myText: String): String {
|
||||||
val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
|
val messageParagraphs = quotedText?.split("\n\n".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()
|
||||||
val quotedTextMsg = StringBuilder()
|
return buildString {
|
||||||
if (messageParagraphs != null) {
|
if (messageParagraphs != null) {
|
||||||
for (i in messageParagraphs.indices) {
|
for (i in messageParagraphs.indices) {
|
||||||
if (messageParagraphs[i].trim() != "") {
|
if (messageParagraphs[i].isNotBlank()) {
|
||||||
quotedTextMsg.append("> ").append(messageParagraphs[i])
|
append("> ")
|
||||||
|
append(messageParagraphs[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i + 1 != messageParagraphs.size) {
|
if (i != messageParagraphs.lastIndex) {
|
||||||
quotedTextMsg.append("\n\n")
|
append("\n\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "$quotedTextMsg\n\n$myText"
|
append("\n\n")
|
||||||
|
append(myText)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
|
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
|
||||||
|
@ -440,7 +445,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSendReaction(action: RoomDetailActions.SendReaction) {
|
private fun handleSendReaction(action: RoomDetailActions.SendReaction) {
|
||||||
room.sendReaction(action.reaction, action.targetEventId)
|
room.sendReaction(action.targetEventId, action.reaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRedactEvent(action: RoomDetailActions.RedactAction) {
|
private fun handleRedactEvent(action: RoomDetailActions.RedactAction) {
|
||||||
|
@ -449,14 +454,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUndoReact(action: RoomDetailActions.UndoReaction) {
|
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) {
|
private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) {
|
||||||
if (action.add) {
|
if (action.add) {
|
||||||
room.sendReaction(action.selectedReaction, action.targetEventId)
|
room.sendReaction(action.targetEventId, action.selectedReaction)
|
||||||
} else {
|
} 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> {})
|
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() {
|
private fun observeSyncState() {
|
||||||
session.rx()
|
session.rx()
|
||||||
.liveSyncState()
|
.liveSyncState()
|
||||||
|
|
|
@ -21,19 +21,18 @@ import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import butterknife.BindView
|
import butterknife.BindView
|
||||||
import butterknife.ButterKnife
|
import butterknife.ButterKnife
|
||||||
import com.airbnb.epoxy.EpoxyRecyclerView
|
|
||||||
import com.airbnb.mvrx.MvRx
|
import com.airbnb.mvrx.MvRx
|
||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.args
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
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 im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
import kotlinx.android.parcel.Parcelize
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@ -48,8 +47,8 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
|
|
||||||
@Inject lateinit var epoxyController: DisplayReadReceiptsController
|
@Inject lateinit var epoxyController: DisplayReadReceiptsController
|
||||||
|
|
||||||
@BindView(R.id.bottom_sheet_display_reactions_list)
|
@BindView(R.id.bottomSheetRecyclerView)
|
||||||
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
lateinit var recyclerView: RecyclerView
|
||||||
|
|
||||||
private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
|
private val displayReadReceiptArgs: DisplayReadReceiptArgs by args()
|
||||||
|
|
||||||
|
@ -58,24 +57,20 @@ class DisplayReadReceiptsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
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)
|
ButterKnife.bind(this, view)
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
epoxyRecyclerView.setController(epoxyController)
|
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||||
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
|
recyclerView.adapter = epoxyController.adapter
|
||||||
LinearLayout.VERTICAL)
|
|
||||||
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
|
|
||||||
bottomSheetTitle.text = getString(R.string.read_at)
|
bottomSheetTitle.text = getString(R.string.read_at)
|
||||||
epoxyController.setData(displayReadReceiptArgs.readReceipts)
|
epoxyController.setData(displayReadReceiptArgs.readReceipts)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() {
|
// we are not using state for this one as it's static, so no need to override invalidate()
|
||||||
// we are not using state for this one as it's static
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(readReceipts: List<ReadReceiptData>): DisplayReadReceiptsBottomSheet {
|
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
|
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
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.lifecycle.ViewModelProviders
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import butterknife.BindView
|
import butterknife.BindView
|
||||||
import butterknife.ButterKnife
|
import butterknife.ButterKnife
|
||||||
import com.airbnb.mvrx.MvRx
|
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
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.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
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 im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import kotlinx.android.synthetic.main.bottom_sheet_message_actions.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bottom sheet fragment that shows a message preview with list of contextual actions
|
* 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)
|
private val viewModel: MessageActionsViewModel by fragmentViewModel(MessageActionsViewModel::class)
|
||||||
|
|
||||||
|
override val showExpanded = true
|
||||||
|
|
||||||
private lateinit var actionHandlerModel: ActionsHandler
|
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) {
|
override fun injectWith(screenComponent: ScreenComponent) {
|
||||||
screenComponent.inject(this)
|
screenComponent.inject(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
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)
|
ButterKnife.bind(this, view)
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
@ -78,78 +62,26 @@ class MessageActionsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
actionHandlerModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
||||||
|
recyclerView.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
|
||||||
val cfm = childFragmentManager
|
recyclerView.adapter = messageActionsEpoxyController.adapter
|
||||||
var menuActionFragment = cfm.findFragmentByTag("MenuActionFragment") as? MessageMenuFragment
|
// Disable item animation
|
||||||
if (menuActionFragment == null) {
|
recyclerView.itemAnimator = null
|
||||||
menuActionFragment = MessageMenuFragment.newInstance(arguments!!.get(MvRx.KEY_ARG) as TimelineEventFragmentArgs)
|
messageActionsEpoxyController.listener = this
|
||||||
cfm.beginTransaction()
|
|
||||||
.replace(R.id.bottom_sheet_menu_container, menuActionFragment, "MenuActionFragment")
|
|
||||||
.commit()
|
|
||||||
}
|
}
|
||||||
menuActionFragment.interactionListener = object : MessageMenuFragment.InteractionListener {
|
|
||||||
override fun didSelectMenuAction(simpleAction: SimpleAction) {
|
override fun didSelectMenuAction(simpleAction: SimpleAction) {
|
||||||
|
if (simpleAction is SimpleAction.ReportContent) {
|
||||||
|
// Toggle report menu
|
||||||
|
viewModel.toggleReportMenu()
|
||||||
|
} else {
|
||||||
actionHandlerModel.fireAction(simpleAction)
|
actionHandlerModel.fireAction(simpleAction)
|
||||||
dismiss()
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) {
|
override fun invalidate() = withState(viewModel) {
|
||||||
val body = viewModel.resolveBody(it)
|
messageActionsEpoxyController.setData(it)
|
||||||
if (body != null) {
|
super.invalidate()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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 dagger.Lazy
|
||||||
import im.vector.matrix.android.api.session.Session
|
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.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.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.MessageTextContent
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
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.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
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.RxRoom
|
||||||
import im.vector.matrix.rx.unwrap
|
import im.vector.matrix.rx.unwrap
|
||||||
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.canReact
|
import im.vector.riotx.core.extensions.canReact
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
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.format.NoticeEventFormatter
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick reactions state
|
||||||
|
*/
|
||||||
|
data class ToggleState(
|
||||||
|
val reaction: String,
|
||||||
|
val isSelected: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
data class MessageActionState(
|
data class MessageActionState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String,
|
val eventId: String,
|
||||||
val informationData: MessageInformationData,
|
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 {
|
) : MvRxState {
|
||||||
|
|
||||||
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
|
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
|
||||||
|
@ -49,17 +71,100 @@ data class MessageActionState(
|
||||||
|
|
||||||
fun senderName(): String = informationData.memberName?.toString() ?: ""
|
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 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()) {
|
return when (timelineEvent()?.root?.getClearType()) {
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE -> {
|
||||||
val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
|
val messageContent: MessageContent? = timelineEvent()?.getLastMessageContent()
|
||||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||||
eventHtmlRenderer?.render(messageContent.formattedBody
|
eventHtmlRenderer.get().render(messageContent.formattedBody
|
||||||
?: messageContent.body)
|
?: messageContent.body)
|
||||||
} else {
|
} else {
|
||||||
messageContent?.body
|
messageContent?.body
|
||||||
|
@ -72,54 +177,177 @@ data class MessageActionState(
|
||||||
EventType.CALL_INVITE,
|
EventType.CALL_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
EventType.CALL_ANSWER -> {
|
EventType.CALL_ANSWER -> {
|
||||||
timelineEvent()?.let { noticeEventFormatter?.format(it) }
|
timelineEvent()?.let { noticeEventFormatter.format(it) }
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
* Information related to an event and used to display preview in contextual bottomsheet.
|
add(SimpleAction.Edit(eventId))
|
||||||
*/
|
|
||||||
class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|
||||||
initialState: MessageActionState,
|
|
||||||
private val eventHtmlRenderer: Lazy<EventHtmlRenderer>,
|
|
||||||
session: Session,
|
|
||||||
private val noticeEventFormatter: NoticeEventFormatter
|
|
||||||
) : VectorViewModel<MessageActionState>(initialState) {
|
|
||||||
|
|
||||||
private val eventId = initialState.eventId
|
|
||||||
private val room = session.getRoom(initialState.roomId)
|
|
||||||
|
|
||||||
@AssistedInject.Factory
|
|
||||||
interface Factory {
|
|
||||||
fun create(initialState: MessageActionState): MessageActionsViewModel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {
|
if (canRedact(event, session.myUserId)) {
|
||||||
|
add(SimpleAction.Delete(eventId))
|
||||||
|
}
|
||||||
|
|
||||||
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
|
if (canCopy(type)) {
|
||||||
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
// TODO copy images? html? see ClipBoard
|
||||||
return fragment.messageActionViewModelFactory.create(state)
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
add(SimpleAction.ViewSource(event.root.toContentStringWithIndent()))
|
||||||
observeEvent()
|
if (event.isEncrypted()) {
|
||||||
|
val decryptedContent = event.root.toClearContentStringWithIndent()
|
||||||
|
?: stringProvider.getString(R.string.encryption_information_decryption_error)
|
||||||
|
add(SimpleAction.ViewDecryptedSource(decryptedContent))
|
||||||
}
|
}
|
||||||
|
add(SimpleAction.CopyPermalink(eventId))
|
||||||
|
|
||||||
private fun observeEvent() {
|
if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
|
||||||
if (room == null) return
|
// not sent by me
|
||||||
RxRoom(room)
|
add(SimpleAction.ReportContent(eventId))
|
||||||
.liveTimelineEvent(eventId)
|
}
|
||||||
.unwrap()
|
}
|
||||||
.execute {
|
|
||||||
copy(timelineEvent = it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resolveBody(state: MessageActionState): CharSequence? {
|
private fun canCancel(@Suppress("UNUSED_PARAMETER") event: TimelineEvent): Boolean {
|
||||||
return state.messageBody(eventHtmlRenderer.get(), noticeEventFormatter)
|
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,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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -21,17 +21,20 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import butterknife.BindView
|
import butterknife.BindView
|
||||||
import butterknife.ButterKnife
|
import butterknife.ButterKnife
|
||||||
import com.airbnb.epoxy.EpoxyRecyclerView
|
|
||||||
import com.airbnb.mvrx.MvRx
|
import com.airbnb.mvrx.MvRx
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
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.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,8 +47,8 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
@Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory
|
@Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory
|
||||||
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
|
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
|
||||||
|
|
||||||
@BindView(R.id.bottom_sheet_display_reactions_list)
|
@BindView(R.id.bottomSheetRecyclerView)
|
||||||
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
lateinit var recyclerView: RecyclerView
|
||||||
|
|
||||||
private val epoxyController by lazy {
|
private val epoxyController by lazy {
|
||||||
ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
|
ViewEditHistoryEpoxyController(requireContext(), viewModel.dateFormatter, eventHtmlRenderer)
|
||||||
|
@ -56,22 +59,23 @@ class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
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)
|
ButterKnife.bind(this, view)
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
epoxyRecyclerView.setController(epoxyController)
|
recyclerView.adapter = epoxyController.adapter
|
||||||
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
|
recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||||
LinearLayout.VERTICAL)
|
val dividerItemDecoration = DividerItemDecoration(requireContext(), LinearLayout.VERTICAL)
|
||||||
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
|
recyclerView.addItemDecoration(dividerItemDecoration)
|
||||||
bottomSheetTitle.text = context?.getString(R.string.message_edits)
|
bottomSheetTitle.text = context?.getString(R.string.message_edits)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) {
|
override fun invalidate() = withState(viewModel) {
|
||||||
epoxyController.setData(it)
|
epoxyController.setData(it)
|
||||||
|
super.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue