mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-01-02 06:17:39 +03:00
Merge branch 'release/0.2.0'
This commit is contained in:
commit
df6080b1da
169 changed files with 3064 additions and 1031 deletions
28
CHANGES.md
28
CHANGES.md
|
@ -1,24 +1,34 @@
|
||||||
Changes in RiotX 0.XX (2019-XX-XX)
|
Changes in RiotX 0.2.0 (2019-07-18)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
- Contextual action menu for messages in room
|
- Message Editing: View edit history (#121)
|
||||||
|
- Rooms filtering (#304)
|
||||||
|
- Edit in encrypted room
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
-
|
- Handle click on redacted events: view source and create permalink
|
||||||
|
- Improve long tap menu: reply on top, more compact (#368)
|
||||||
|
- Quick reply in timeline with swipe gesture (#167)
|
||||||
|
- Improve edit of replies
|
||||||
|
- Improve performance on Room Members and Users management (#381)
|
||||||
|
|
||||||
Other changes:
|
Other changes:
|
||||||
-
|
- migrate from rxbinding 2 to rxbinding 3
|
||||||
|
|
||||||
Bugfix:
|
Bugfix:
|
||||||
-
|
- Fix regression on permalink click
|
||||||
|
- Fix crash reported by the PlayStore (#341)
|
||||||
|
- Fix Chat composer separator color in dark/black theme
|
||||||
|
- Fix bad layout for room directory filter (#349)
|
||||||
|
- Fix Copying link from a message shouldn't open context menu (#364)
|
||||||
|
|
||||||
Translations:
|
Changes in RiotX 0.1.0 (2019-07-11)
|
||||||
-
|
===================================================
|
||||||
|
|
||||||
Build:
|
First release!
|
||||||
-
|
|
||||||
|
|
||||||
|
Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-android-b17952e8f771
|
||||||
|
|
||||||
|
|
||||||
=======================================================
|
=======================================================
|
||||||
|
|
29
README.md
29
README.md
|
@ -1,4 +1,4 @@
|
||||||
[![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android)
|
[![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
||||||
[![Weblate](https://translate.riot.im/widgets/riot-android/-/svg-badge.svg)](https://translate.riot.im/engage/riot-android/?utm_source=widget)
|
[![Weblate](https://translate.riot.im/widgets/riot-android/-/svg-badge.svg)](https://translate.riot.im/engage/riot-android/?utm_source=widget)
|
||||||
[![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org)
|
[![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org)
|
||||||
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=alert_status)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=alert_status)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
||||||
|
@ -7,14 +7,31 @@
|
||||||
|
|
||||||
# RiotX Android
|
# RiotX Android
|
||||||
|
|
||||||
RiotX is an Android Matrix Client currently in development. The application is not yet available on the PlayStore.
|
RiotX is an Android Matrix Client currently in beta but in active development.
|
||||||
|
|
||||||
It's based on a new Matrix SDK, written in Kotlin.
|
It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience. RiotX will become the official replacement as soon as all features are implemented.
|
||||||
|
|
||||||
Download nightly build here: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx)
|
||||||
|
|
||||||
|
Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
||||||
|
|
||||||
|
# New Android SDK
|
||||||
|
|
||||||
|
RiotX is based on a new Android SDK fully written in Kotlin (like RiotX). In order to make the early development as fast as possible, RiotX and the new SDK currently share the same git repository. We will make separate repos once the API is stable enough.
|
||||||
|
|
||||||
|
|
||||||
|
# Roadmap
|
||||||
|
|
||||||
|
The current target is to release an application out of beta with the same level of features (and even more) as Riot.
|
||||||
|
The roadmap has 3 phases:
|
||||||
|
|
||||||
|
- [phase 0](https://github.com/vector-im/riotX-android/labels/phase0): Prototyping / Project setup
|
||||||
|
- [phase 1](https://github.com/vector-im/riotX-android/labels/phase1): Beta release to the Play Store
|
||||||
|
- [phase 2](https://github.com/vector-im/riotX-android/labels/phase2): Out of beta
|
||||||
|
|
||||||
Matrix Room: [![RiotX Android Matrix room #riot-android:matrix.org](https://img.shields.io/matrix/riotx:matrix.org.svg?label=%23RiotX:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#riotx:matrix.org)
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute the Matrix on Android projects!
|
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!
|
||||||
|
|
||||||
|
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#riotx:matrix.org).
|
||||||
|
|
21
build.gradle
21
build.gradle
|
@ -1,3 +1,5 @@
|
||||||
|
import javax.tools.JavaCompiler
|
||||||
|
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
|
@ -45,7 +47,26 @@ allprojects {
|
||||||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
|
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
maven {
|
||||||
|
url 'https://repo.adobe.com/nexus/content/repositories/public/'
|
||||||
|
content {
|
||||||
|
includeGroupByRegex "diff_match_patch"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile).all {
|
||||||
|
options.compilerArgs += [
|
||||||
|
'-Adagger.gradle.incremental=enabled'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
extensions.findByName("kapt")?.arguments {
|
||||||
|
arg("dagger.gradle.incremental", "enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
task clean(type: Delete) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
This document aims to describe how Riot X android displays notifications to the end user. It also clarifies notifications and background settings in the app.
|
This document aims to describe how RiotX android displays notifications to the end user. It also clarifies notifications and background settings in the app.
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
1. [Prerequisites Knowledge](#prerequisites-knowledge)
|
1. [Prerequisites Knowledge](#prerequisites-knowledge)
|
||||||
|
@ -50,7 +50,7 @@ By default, this is 0, so the server will return immediately even if the respons
|
||||||
|
|
||||||
**delay** is a client preference. When the server responds to a sync request, the client waits for `delay`before calling a new sync.
|
**delay** is a client preference. When the server responds to a sync request, the client waits for `delay`before calling a new sync.
|
||||||
|
|
||||||
When the Riot X Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
|
When the RiotX Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
|
||||||
|
|
||||||
## How does a mobile app receives push notification
|
## How does a mobile app receives push notification
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ This need some disambiguation, because it is the source of common confusion:
|
||||||
In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication!
|
In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication!
|
||||||
This server is called a **Push Gateway** in the matrix world
|
This server is called a **Push Gateway** in the matrix world
|
||||||
|
|
||||||
That means that Riot X Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.
|
That means that RiotX Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.
|
||||||
|
|
||||||
If you create your own matrix client, you will also need to deploy an instance of a **Push Gateway** with the credentials needed to use FCM for your app.
|
If you create your own matrix client, you will also need to deploy an instance of a **Push Gateway** with the credentials needed to use FCM for your app.
|
||||||
|
|
||||||
|
@ -223,7 +223,7 @@ Upon reception of the FCM push, RiotX will perform a sync call to the Home Serve
|
||||||
* The sync generates additional notifications (e.g an encrypted message where the user is mentioned detected locally)
|
* The sync generates additional notifications (e.g an encrypted message where the user is mentioned detected locally)
|
||||||
* The sync takes too long and the process is killed before completion, or network is not reliable and the sync fails.
|
* The sync takes too long and the process is killed before completion, or network is not reliable and the sync fails.
|
||||||
|
|
||||||
Riot X implements several strategies in these cases (TODO document)
|
RiotX implements several strategies in these cases (TODO document)
|
||||||
|
|
||||||
## FCM Fallback mode
|
## FCM Fallback mode
|
||||||
|
|
||||||
|
|
|
@ -99,14 +99,14 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
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-beta01"
|
implementation "androidx.appcompat:appcompat:1.1.0-rc01"
|
||||||
implementation "androidx.recyclerview:recyclerview:1.1.0-alpha06"
|
implementation "androidx.recyclerview:recyclerview:1.1.0-beta01"
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
|
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
|
||||||
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
|
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:3.14.1'
|
implementation 'com.squareup.okhttp3:okhttp:3.14.1'
|
||||||
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
|
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
|
||||||
|
|
|
@ -42,7 +42,7 @@ object MatrixLinkify {
|
||||||
hasMatch = true
|
hasMatch = true
|
||||||
val startPos = match.range.first
|
val startPos = match.range.first
|
||||||
if (startPos == 0 || text[startPos - 1] != '/') {
|
if (startPos == 0 || text[startPos - 1] != '/') {
|
||||||
val endPos = match.range.last
|
val endPos = match.range.last + 1
|
||||||
val url = text.substring(match.range)
|
val url = text.substring(match.range)
|
||||||
val span = MatrixPermalinkSpan(url, callback)
|
val span = MatrixPermalinkSpan(url, callback)
|
||||||
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
|
@ -51,5 +51,5 @@ object MatrixLinkify {
|
||||||
}
|
}
|
||||||
return hasMatch
|
return hasMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -57,6 +57,9 @@ interface Session :
|
||||||
*/
|
*/
|
||||||
val sessionParams: SessionParams
|
val sessionParams: SessionParams
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Useful shortcut to get access to the userId
|
||||||
|
*/
|
||||||
val myUserId: String
|
val myUserId: String
|
||||||
get() = sessionParams.credentials.userId
|
get() = sessionParams.credentials.userId
|
||||||
|
|
||||||
|
@ -84,7 +87,7 @@ interface Session :
|
||||||
/**
|
/**
|
||||||
* This method start the sync thread.
|
* This method start the sync thread.
|
||||||
*/
|
*/
|
||||||
fun startSync()
|
fun startSync(fromForeground : Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method stop the sync thread.
|
* This method stop the sync thread.
|
||||||
|
|
|
@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.events.model
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constants defining known event relation types from Matrix specifications.
|
* Constants defining known event relation types from Matrix specifications
|
||||||
*/
|
*/
|
||||||
object RelationType {
|
object RelationType {
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ object RelationType {
|
||||||
const val ANNOTATION = "m.annotation"
|
const val ANNOTATION = "m.annotation"
|
||||||
/** Lets you define an event which replaces an existing event.*/
|
/** Lets you define an event which replaces an existing event.*/
|
||||||
const val REPLACE = "m.replace"
|
const val REPLACE = "m.replace"
|
||||||
/** ets you define an event which references an existing event.*/
|
/** Lets you define an event which references an existing event.*/
|
||||||
const val REFERENCE = "m.reference"
|
const val REFERENCE = "m.reference"
|
||||||
|
|
||||||
}
|
}
|
|
@ -25,4 +25,9 @@ interface MessageContent {
|
||||||
val body: String
|
val body: String
|
||||||
val relatesTo: RelationDefaultContent?
|
val relatesTo: RelationDefaultContent?
|
||||||
val newContent: Content?
|
val newContent: Content?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun MessageContent?.isReply(): Boolean {
|
||||||
|
return this?.relatesTo?.inReplyTo != null
|
||||||
}
|
}
|
|
@ -16,7 +16,10 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.model.relation
|
package im.vector.matrix.android.api.session.room.model.relation
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||||
|
|
||||||
interface RelationContent {
|
interface RelationContent {
|
||||||
|
/** See [RelationType] for known possible values */
|
||||||
val type: String?
|
val type: String?
|
||||||
val eventId: String?
|
val eventId: String?
|
||||||
val inReplyTo: ReplyToContent?
|
val inReplyTo: ReplyToContent?
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
package im.vector.matrix.android.api.session.room.model.relation
|
package im.vector.matrix.android.api.session.room.model.relation
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
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.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
@ -79,6 +81,25 @@ interface RelationService {
|
||||||
compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit a reply. This is a special case because replies contains fallback text as a prefix.
|
||||||
|
* This method will take the new body (stripped from fallbacks) and re-add them before sending.
|
||||||
|
* @param replyToEdit The event to edit
|
||||||
|
* @param originalTimelineEvent the message that this reply (being edited) is relating to
|
||||||
|
* @param newBodyText The edited body (stripped from in reply to content)
|
||||||
|
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
|
||||||
|
*/
|
||||||
|
fun editReply(replyToEdit: TimelineEvent,
|
||||||
|
originalTimelineEvent: TimelineEvent,
|
||||||
|
newBodyText: String,
|
||||||
|
compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get's the edit history of the given event
|
||||||
|
*/
|
||||||
|
fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reply to an event in the timeline (must be in same room)
|
* Reply to an event in the timeline (must be in same room)
|
||||||
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
|
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
|
||||||
|
@ -91,4 +112,6 @@ interface RelationService {
|
||||||
autoMarkdown: Boolean = false): Cancelable?
|
autoMarkdown: Boolean = false): Cancelable?
|
||||||
|
|
||||||
fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary>
|
fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary>
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -21,7 +21,10 @@ 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.EventAnnotationsSummary
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
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.isReply
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
|
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
|
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
|
||||||
|
@ -88,3 +91,15 @@ data class TimelineEvent(
|
||||||
*/
|
*/
|
||||||
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
|
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
|
||||||
?: root.getClearContent().toModel()
|
?: root.getClearContent().toModel()
|
||||||
|
|
||||||
|
|
||||||
|
fun TimelineEvent.getTextEditableContent(): String? {
|
||||||
|
val originalContent = root.getClearContent().toModel<MessageContent>() ?: return null
|
||||||
|
val isReply = originalContent.isReply() || root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId != null
|
||||||
|
val lastContent = getLastMessageContent()
|
||||||
|
return if (isReply) {
|
||||||
|
return extractUsefulTextFromReply(lastContent?.body ?: "")
|
||||||
|
} else {
|
||||||
|
lastContent?.body ?: ""
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ package im.vector.matrix.android.api.session.sync
|
||||||
|
|
||||||
sealed class SyncState {
|
sealed class SyncState {
|
||||||
object IDLE : SyncState()
|
object IDLE : SyncState()
|
||||||
data class RUNNING(val catchingUp: Boolean) : SyncState()
|
data class RUNNING(val afterPause: Boolean) : SyncState()
|
||||||
object PAUSED : SyncState()
|
object PAUSED : SyncState()
|
||||||
object KILLING : SyncState()
|
object KILLING : SyncState()
|
||||||
object KILLED : SyncState()
|
object KILLED : SyncState()
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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.util
|
||||||
|
|
||||||
|
|
||||||
|
object ContentUtils {
|
||||||
|
fun extractUsefulTextFromReply(repliedBody: String): String {
|
||||||
|
val lines = repliedBody.lines()
|
||||||
|
var wellFormed = repliedBody.startsWith(">")
|
||||||
|
var endOfPreviousFound = false
|
||||||
|
val usefullines = ArrayList<String>()
|
||||||
|
lines.forEach {
|
||||||
|
if (it == "") {
|
||||||
|
endOfPreviousFound = true
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
if (!endOfPreviousFound) {
|
||||||
|
wellFormed = wellFormed && it.startsWith(">")
|
||||||
|
} else {
|
||||||
|
usefullines.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
|
||||||
|
}
|
||||||
|
|
||||||
|
fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
|
||||||
|
if (repliedBody.startsWith("<mx-reply>")) {
|
||||||
|
val closingTagIndex = repliedBody.lastIndexOf("</mx-reply>")
|
||||||
|
if (closingTagIndex != -1)
|
||||||
|
return repliedBody.substring(closingTagIndex + "</mx-reply>".length).trim()
|
||||||
|
}
|
||||||
|
return repliedBody
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,16 +50,10 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
|
private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
|
||||||
val userId = sessionParams.credentials.userId
|
return sessionComponents.getOrPut(sessionParams.credentials.userId) {
|
||||||
if (sessionComponents.containsKey(userId)) {
|
DaggerSessionComponent
|
||||||
return sessionComponents[userId]!!
|
.factory()
|
||||||
|
.create(matrixComponent, sessionParams)
|
||||||
}
|
}
|
||||||
return DaggerSessionComponent
|
|
||||||
.factory()
|
|
||||||
.create(matrixComponent, sessionParams)
|
|
||||||
.also {
|
|
||||||
sessionComponents[sessionParams.credentials.userId] = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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 android.text.TextUtils
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
@ -80,10 +79,9 @@ import im.vector.matrix.android.internal.util.fetchCopied
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.matrix.olm.OlmManager
|
import org.matrix.olm.OlmManager
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A `CryptoService` class instance manages the end-to-end crypto for a session.
|
* A `CryptoService` class instance manages the end-to-end crypto for a session.
|
||||||
|
@ -248,7 +246,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isStarting.set(true)
|
isStarting.set(true)
|
||||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||||
internalStart(isInitialSync)
|
internalStart(isInitialSync)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -315,7 +313,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
* @param syncResponse the syncResponse
|
* @param syncResponse the syncResponse
|
||||||
*/
|
*/
|
||||||
fun onSyncCompleted(syncResponse: SyncResponse) {
|
fun onSyncCompleted(syncResponse: SyncResponse) {
|
||||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||||
if (syncResponse.deviceLists != null) {
|
if (syncResponse.deviceLists != null) {
|
||||||
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
|
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
|
||||||
}
|
}
|
||||||
|
@ -340,7 +338,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
* @return the device info, or null if not found / unsupported algorithm / crypto released
|
* @return the device info, or null if not found / unsupported algorithm / crypto released
|
||||||
*/
|
*/
|
||||||
override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? {
|
override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? {
|
||||||
return if (!TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_MEGOLM) && !TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_OLM)) {
|
return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) {
|
||||||
// We only deal in olm keys
|
// We only deal in olm keys
|
||||||
null
|
null
|
||||||
} else cryptoStore.deviceWithIdentityKey(senderKey)
|
} else cryptoStore.deviceWithIdentityKey(senderKey)
|
||||||
|
@ -353,8 +351,8 @@ internal class CryptoManager @Inject constructor(
|
||||||
* @param deviceId the device id
|
* @param deviceId the device id
|
||||||
*/
|
*/
|
||||||
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
|
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
|
||||||
return if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) {
|
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
|
||||||
cryptoStore.getUserDevice(deviceId!!, userId)
|
cryptoStore.getUserDevice(deviceId, userId)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -439,7 +437,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
// (for now at least. Maybe we should alert the user somehow?)
|
// (for now at least. Maybe we should alert the user somehow?)
|
||||||
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
|
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(existingAlgorithm) && !TextUtils.equals(existingAlgorithm, algorithm)) {
|
if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) {
|
||||||
Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
|
Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -535,7 +533,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
eventType: String,
|
eventType: String,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
callback: MatrixCallback<MXEncryptEventContentResult>) {
|
callback: MatrixCallback<MXEncryptEventContentResult>) {
|
||||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||||
if (!isStarted()) {
|
if (!isStarted()) {
|
||||||
Timber.v("## encryptEventContent() : wait after e2e init")
|
Timber.v("## encryptEventContent() : wait after e2e init")
|
||||||
internalStart(false)
|
internalStart(false)
|
||||||
|
@ -601,7 +599,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
* @param callback the callback to return data or null
|
* @param callback the callback to return data or null
|
||||||
*/
|
*/
|
||||||
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
||||||
GlobalScope.launch(EmptyCoroutineContext) {
|
GlobalScope.launch {
|
||||||
val result = withContext(coroutineDispatchers.crypto) {
|
val result = withContext(coroutineDispatchers.crypto) {
|
||||||
internalDecryptEvent(event, timeline)
|
internalDecryptEvent(event, timeline)
|
||||||
}
|
}
|
||||||
|
@ -649,7 +647,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
fun onToDeviceEvent(event: Event) {
|
fun onToDeviceEvent(event: Event) {
|
||||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||||
when (event.getClearType()) {
|
when (event.getClearType()) {
|
||||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||||
onRoomKeyEvent(event)
|
onRoomKeyEvent(event)
|
||||||
|
@ -671,7 +669,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
*/
|
*/
|
||||||
private fun onRoomKeyEvent(event: Event) {
|
private fun onRoomKeyEvent(event: Event) {
|
||||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
||||||
if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.algorithm)) {
|
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
||||||
Timber.e("## onRoomKeyEvent() : missing fields")
|
Timber.e("## onRoomKeyEvent() : missing fields")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -689,7 +687,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
* @param event the encryption event.
|
* @param event the encryption event.
|
||||||
*/
|
*/
|
||||||
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
|
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
|
||||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||||
val params = LoadRoomMembersTask.Params(roomId)
|
val params = LoadRoomMembersTask.Params(roomId)
|
||||||
loadRoomMembersTask
|
loadRoomMembersTask
|
||||||
.execute(params)
|
.execute(params)
|
||||||
|
@ -738,7 +736,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
val membership = roomMember?.membership
|
val membership = roomMember?.membership
|
||||||
if (membership == Membership.JOIN) {
|
if (membership == Membership.JOIN) {
|
||||||
// make sure we are tracking the deviceList for this user.
|
// make sure we are tracking the deviceList for this user.
|
||||||
deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
|
deviceListManager.startTrackingDeviceList(listOf(userId))
|
||||||
} else if (membership == Membership.INVITE
|
} else if (membership == Membership.INVITE
|
||||||
&& shouldEncryptForInvitedMembers(roomId)
|
&& shouldEncryptForInvitedMembers(roomId)
|
||||||
&& cryptoConfig.enableEncryptionForInvitedMembers) {
|
&& cryptoConfig.enableEncryptionForInvitedMembers) {
|
||||||
|
@ -747,7 +745,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
// know what other servers are in the room at the time they've been invited.
|
// know what other servers are in the room at the time they've been invited.
|
||||||
// They therefore will not send device updates if a user logs in whilst
|
// They therefore will not send device updates if a user logs in whilst
|
||||||
// their state is invite.
|
// their state is invite.
|
||||||
deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
|
deviceListManager.startTrackingDeviceList(listOf(userId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -782,7 +780,11 @@ internal class CryptoManager @Inject constructor(
|
||||||
* @param callback the exported keys
|
* @param callback the exported keys
|
||||||
*/
|
*/
|
||||||
override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
|
override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
|
||||||
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT, callback)
|
GlobalScope.launch(coroutineDispatchers.main) {
|
||||||
|
runCatching {
|
||||||
|
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
|
||||||
|
}.fold(callback::onSuccess, callback::onFailure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -792,30 +794,16 @@ internal class CryptoManager @Inject constructor(
|
||||||
* @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
|
* @param callback the exported keys
|
||||||
*/
|
*/
|
||||||
private fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) {
|
private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray {
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
return withContext(coroutineDispatchers.crypto) {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
val iterationCount = max(0, anIterationCount)
|
||||||
Try {
|
|
||||||
val iterationCount = Math.max(0, anIterationCount)
|
|
||||||
|
|
||||||
val exportedSessions = ArrayList<MegolmSessionData>()
|
val exportedSessions = cryptoStore.getInboundGroupSessions().mapNotNull { it.exportKeys() }
|
||||||
|
|
||||||
val inboundGroupSessions = cryptoStore.getInboundGroupSessions()
|
val adapter = MoshiProvider.providesMoshi()
|
||||||
|
.adapter(List::class.java)
|
||||||
|
|
||||||
for (session in inboundGroupSessions) {
|
MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
|
||||||
val megolmSessionData = session.exportKeys()
|
|
||||||
|
|
||||||
if (null != megolmSessionData) {
|
|
||||||
exportedSessions.add(megolmSessionData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val adapter = MoshiProvider.providesMoshi()
|
|
||||||
.adapter(List::class.java)
|
|
||||||
|
|
||||||
MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
|
|
||||||
}
|
|
||||||
}.foldToCallback(callback)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -879,7 +867,7 @@ internal class CryptoManager @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
|
||||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||||
deviceListManager
|
deviceListManager
|
||||||
.downloadKeys(userIds, true)
|
.downloadKeys(userIds, true)
|
||||||
.fold(
|
.fold(
|
||||||
|
@ -944,7 +932,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList()
|
val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList()
|
||||||
|
|
||||||
if (add) {
|
if (add) {
|
||||||
if (!roomIds.contains(roomId)) {
|
if (roomId !in roomIds) {
|
||||||
roomIds.add(roomId)
|
roomIds.add(roomId)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1033,8 +1021,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
|
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
|
||||||
val userIds = devicesInRoom.userIds
|
val userIds = devicesInRoom.userIds
|
||||||
for (userId in userIds) {
|
for (userId in userIds) {
|
||||||
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
|
devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId ->
|
||||||
deviceIds?.forEach { deviceId ->
|
|
||||||
devicesInRoom.getObject(userId, deviceId)
|
devicesInRoom.getObject(userId, deviceId)
|
||||||
?.takeIf { it.isUnknown }
|
?.takeIf { it.isUnknown }
|
||||||
?.let {
|
?.let {
|
||||||
|
@ -1047,7 +1034,7 @@ internal class CryptoManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
|
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
|
||||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||||
deviceListManager
|
deviceListManager
|
||||||
.downloadKeys(userIds, forceDownload)
|
.downloadKeys(userIds, forceDownload)
|
||||||
.foldToCallback(callback)
|
.foldToCallback(callback)
|
||||||
|
|
|
@ -29,7 +29,6 @@ import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevi
|
||||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||||
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.keysbackup.KeysBackup
|
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
|
||||||
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.model.event.EncryptedEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||||
|
@ -38,10 +37,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
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.tasks.SendToDeviceTask
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
internal class MXMegolmDecryption(private val credentials: Credentials,
|
internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||||
|
@ -312,7 +310,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val userId = request.userId ?: return
|
val userId = request.userId ?: return
|
||||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||||
deviceListManager
|
deviceListManager
|
||||||
.downloadKeys(listOf(userId), false)
|
.downloadKeys(listOf(userId), false)
|
||||||
.flatMap {
|
.flatMap {
|
||||||
|
@ -321,8 +319,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||||
if (deviceInfo == null) {
|
if (deviceInfo == null) {
|
||||||
throw RuntimeException()
|
throw RuntimeException()
|
||||||
} else {
|
} else {
|
||||||
val devicesByUser = HashMap<String, List<MXDeviceInfo>>()
|
val devicesByUser = mapOf(userId to listOf(deviceInfo))
|
||||||
devicesByUser[userId] = ArrayList(Arrays.asList(deviceInfo))
|
|
||||||
ensureOlmSessionsForDevicesAction
|
ensureOlmSessionsForDevicesAction
|
||||||
.handle(devicesByUser)
|
.handle(devicesByUser)
|
||||||
.flatMap {
|
.flatMap {
|
||||||
|
@ -336,8 +333,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||||
Timber.v("## shareKeysWithDevice() : sharing keys for session" +
|
Timber.v("## shareKeysWithDevice() : sharing keys for session" +
|
||||||
" ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId")
|
" ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId")
|
||||||
|
|
||||||
val payloadJson = HashMap<String, Any>()
|
val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
|
||||||
payloadJson["type"] = EventType.FORWARDED_ROOM_KEY
|
|
||||||
|
|
||||||
olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId)
|
olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId)
|
||||||
.fold(
|
.fold(
|
||||||
|
@ -350,7 +346,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, Arrays.asList(deviceInfo))
|
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
|
||||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||||
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
|
||||||
Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId")
|
Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId")
|
||||||
|
|
|
@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto.model.event
|
||||||
|
|
||||||
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.room.model.relation.RelationDefaultContent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing an encrypted event content
|
* Class representing an encrypted event content
|
||||||
|
@ -52,5 +53,8 @@ data class EncryptedEventContent(
|
||||||
* The session id
|
* The session id
|
||||||
*/
|
*/
|
||||||
@Json(name = "session_id")
|
@Json(name = "session_id")
|
||||||
val sessionId: String? = null
|
val sessionId: String? = null,
|
||||||
|
|
||||||
|
//Relation context is in clear in encrypted message
|
||||||
|
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? = null
|
||||||
)
|
)
|
|
@ -40,7 +40,7 @@ import im.vector.matrix.android.internal.session.SessionScope
|
||||||
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 im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -71,7 +71,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
|
||||||
|
|
||||||
// Event received from the sync
|
// Event received from the sync
|
||||||
fun onToDeviceEvent(event: Event) {
|
fun onToDeviceEvent(event: Event) {
|
||||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||||
when (event.getClearType()) {
|
when (event.getClearType()) {
|
||||||
EventType.KEY_VERIFICATION_START -> {
|
EventType.KEY_VERIFICATION_START -> {
|
||||||
onStartRequestReceived(event)
|
onStartRequestReceived(event)
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.database.helper
|
package im.vector.matrix.android.internal.database.helper
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
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.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
|
@ -103,7 +102,6 @@ internal fun ChunkEntity.updateSenderDataFor(eventIds: List<String>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun ChunkEntity.add(roomId: String,
|
internal fun ChunkEntity.add(roomId: String,
|
||||||
event: Event,
|
event: Event,
|
||||||
direction: PaginationDirection,
|
direction: PaginationDirection,
|
||||||
|
@ -134,7 +132,7 @@ internal fun ChunkEntity.add(roomId: String,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val localId = TimelineEventEntity.nextId(realm)
|
val localId = TimelineEventEntity.nextId(realm)
|
||||||
val eventEntity = TimelineEventEntity(localId).also {
|
val eventEntity = TimelineEventEntity(localId).also {
|
||||||
it.root = event.toEntity(roomId).apply {
|
it.root = event.toEntity(roomId).apply {
|
||||||
this.stateIndex = currentStateIndex
|
this.stateIndex = currentStateIndex
|
||||||
|
|
|
@ -37,25 +37,22 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
|
internal fun RoomEntity.addStateEvent(stateEvent: Event,
|
||||||
stateIndex: Int = Int.MIN_VALUE,
|
stateIndex: Int = Int.MIN_VALUE,
|
||||||
filterDuplicates: Boolean = false,
|
filterDuplicates: Boolean = false,
|
||||||
isUnlinked: Boolean = false) {
|
isUnlinked: Boolean = false) {
|
||||||
assertIsManaged()
|
assertIsManaged()
|
||||||
|
if (stateEvent.eventId == null || (filterDuplicates && fastContains(stateEvent.eventId))) {
|
||||||
stateEvents.forEach { event ->
|
return
|
||||||
if (event.eventId == null || (filterDuplicates && fastContains(event.eventId))) {
|
} else {
|
||||||
return@forEach
|
val entity = stateEvent.toEntity(roomId).apply {
|
||||||
}
|
|
||||||
val eventEntity = event.toEntity(roomId).apply {
|
|
||||||
this.stateIndex = stateIndex
|
this.stateIndex = stateIndex
|
||||||
this.isUnlinked = isUnlinked
|
this.isUnlinked = isUnlinked
|
||||||
this.sendState = SendState.SYNCED
|
this.sendState = SendState.SYNCED
|
||||||
}
|
}
|
||||||
untimelinedStateEvents.add(0, eventEntity)
|
untimelinedStateEvents.add(entity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun RoomEntity.addSendingEvent(event: Event) {
|
internal fun RoomEntity.addSendingEvent(event: Event) {
|
||||||
assertIsManaged()
|
assertIsManaged()
|
||||||
val senderId = event.senderId ?: return
|
val senderId = event.senderId ?: return
|
||||||
|
@ -64,7 +61,7 @@ internal fun RoomEntity.addSendingEvent(event: Event) {
|
||||||
}
|
}
|
||||||
val roomMembers = RoomMembers(realm, roomId)
|
val roomMembers = RoomMembers(realm, roomId)
|
||||||
val myUser = roomMembers.get(senderId)
|
val myUser = roomMembers.get(senderId)
|
||||||
val localId = TimelineEventEntity.nextId(realm)
|
val localId = TimelineEventEntity.nextId(realm)
|
||||||
val timelineEventEntity = TimelineEventEntity(localId).also {
|
val timelineEventEntity = TimelineEventEntity(localId).also {
|
||||||
it.root = eventEntity
|
it.root = eventEntity
|
||||||
it.eventId = event.eventId ?: ""
|
it.eventId = event.eventId ?: ""
|
||||||
|
|
|
@ -20,8 +20,6 @@ import io.realm.RealmObject
|
||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
import io.realm.annotations.Index
|
import io.realm.annotations.Index
|
||||||
import io.realm.annotations.LinkingObjects
|
import io.realm.annotations.LinkingObjects
|
||||||
import io.realm.annotations.PrimaryKey
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
|
||||||
internal open class TimelineEventEntity(var localId: Long = 0,
|
internal open class TimelineEventEntity(var localId: Long = 0,
|
||||||
|
|
|
@ -18,7 +18,8 @@ package im.vector.matrix.android.internal.network
|
||||||
|
|
||||||
internal object NetworkConstants {
|
internal object NetworkConstants {
|
||||||
|
|
||||||
const val URI_API_PREFIX_PATH = "_matrix/client/"
|
private const val URI_API_PREFIX_PATH = "_matrix/client"
|
||||||
const val URI_API_PREFIX_PATH_R0 = "_matrix/client/r0/"
|
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
|
||||||
|
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,21 +19,15 @@ package im.vector.matrix.android.internal.network
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import arrow.core.failure
|
import arrow.core.failure
|
||||||
import arrow.core.recoverWith
|
import arrow.core.recoverWith
|
||||||
import arrow.effects.IO
|
|
||||||
import arrow.effects.fix
|
|
||||||
import arrow.effects.instances.io.async.async
|
|
||||||
import arrow.integrations.retrofit.adapter.runAsync
|
|
||||||
import com.squareup.moshi.JsonDataException
|
import com.squareup.moshi.JsonDataException
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
import im.vector.matrix.android.api.failure.MatrixError
|
import im.vector.matrix.android.api.failure.MatrixError
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
internal suspend inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute()
|
internal suspend inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute()
|
||||||
|
|
||||||
|
@ -43,30 +37,22 @@ internal class Request<DATA> {
|
||||||
lateinit var apiCall: Call<DATA>
|
lateinit var apiCall: Call<DATA>
|
||||||
|
|
||||||
suspend fun execute(): Try<DATA> {
|
suspend fun execute(): Try<DATA> {
|
||||||
return suspendCancellableCoroutine { continuation ->
|
return Try {
|
||||||
continuation.invokeOnCancellation {
|
val response = apiCall.awaitResponse()
|
||||||
Timber.v("Request is canceled")
|
if (response.isSuccessful) {
|
||||||
apiCall.cancel()
|
response.body()
|
||||||
|
?: throw IllegalStateException("The request returned a null body")
|
||||||
|
} else {
|
||||||
|
throw manageFailure(response.errorBody(), response.code())
|
||||||
}
|
}
|
||||||
val result = Try {
|
}.recoverWith {
|
||||||
val response = apiCall.runAsync(IO.async()).fix().unsafeRunSync()
|
when (it) {
|
||||||
if (response.isSuccessful) {
|
is IOException -> Failure.NetworkConnection(it)
|
||||||
response.body()
|
is Failure.ServerError,
|
||||||
?: throw IllegalStateException("The request returned a null body")
|
is Failure.OtherServerError -> it
|
||||||
} else {
|
else -> Failure.Unknown(it)
|
||||||
throw manageFailure(response.errorBody(), response.code())
|
}.failure()
|
||||||
}
|
|
||||||
}.recoverWith {
|
|
||||||
when (it) {
|
|
||||||
is IOException -> Failure.NetworkConnection(it)
|
|
||||||
is Failure.ServerError,
|
|
||||||
is Failure.OtherServerError -> it
|
|
||||||
else -> Failure.Unknown(it)
|
|
||||||
}.failure()
|
|
||||||
}
|
|
||||||
continuation.resume(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable {
|
private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable {
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* * 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.network
|
||||||
|
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import retrofit2.*
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
|
suspend fun <T> Call<T>.awaitResponse(): Response<T> {
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
enqueue(object : Callback<T> {
|
||||||
|
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||||
|
continuation.resume(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||||
|
continuation.resumeWithException(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -94,19 +94,21 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requireBackgroundSync() {
|
override fun requireBackgroundSync() {
|
||||||
SyncWorker.requireBackgroundSync(context, sessionParams.credentials.userId)
|
SyncWorker.requireBackgroundSync(context, myUserId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startAutomaticBackgroundSync(repeatDelay: Long) {
|
override fun startAutomaticBackgroundSync(repeatDelay: Long) {
|
||||||
SyncWorker.automaticallyBackgroundSync(context, sessionParams.credentials.userId, 0, repeatDelay)
|
SyncWorker.automaticallyBackgroundSync(context, myUserId, 0, repeatDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopAnyBackgroundSync() {
|
override fun stopAnyBackgroundSync() {
|
||||||
SyncWorker.stopAnyBackgroundSync(context)
|
SyncWorker.stopAnyBackgroundSync(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun startSync() {
|
override fun startSync(fromForeground : Boolean) {
|
||||||
|
Timber.i("Starting sync thread")
|
||||||
assert(isOpen)
|
assert(isOpen)
|
||||||
|
syncThread.setInitialForeground(fromForeground)
|
||||||
if (!syncThread.isAlive) {
|
if (!syncThread.isAlive) {
|
||||||
syncThread.start()
|
syncThread.start()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -38,7 +38,6 @@ import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
|
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
|
||||||
import im.vector.matrix.android.internal.session.user.UserEntityUpdater
|
|
||||||
import im.vector.matrix.android.internal.util.md5
|
import im.vector.matrix.android.internal.util.md5
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
@ -129,10 +128,6 @@ internal abstract class SessionModule {
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver
|
abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver
|
||||||
|
|
||||||
@Binds
|
|
||||||
@IntoSet
|
|
||||||
abstract fun bindUserEntityUpdater(groupSummaryUpdater: UserEntityUpdater): LiveEntityObserver
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService
|
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,13 @@ package im.vector.matrix.android.internal.session.room
|
||||||
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
|
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||||
import im.vector.matrix.android.api.session.events.model.*
|
import im.vector.matrix.android.api.session.events.model.*
|
||||||
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.relation.ReactionContent
|
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||||
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
||||||
import im.vector.matrix.android.internal.database.model.*
|
import im.vector.matrix.android.internal.database.model.*
|
||||||
|
@ -43,7 +47,9 @@ internal interface EventRelationsAggregationTask : Task<EventRelationsAggregatio
|
||||||
/**
|
/**
|
||||||
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
|
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
|
||||||
*/
|
*/
|
||||||
internal class DefaultEventRelationsAggregationTask @Inject constructor(private val monarchy: Monarchy) : EventRelationsAggregationTask {
|
internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
||||||
|
private val monarchy: Monarchy,
|
||||||
|
private val cryptoService: CryptoService) : EventRelationsAggregationTask {
|
||||||
|
|
||||||
//OPT OUT serer aggregation until API mature enough
|
//OPT OUT serer aggregation until API mature enough
|
||||||
private val SHOULD_HANDLE_SERVER_AGREGGATION = false
|
private val SHOULD_HANDLE_SERVER_AGREGGATION = false
|
||||||
|
@ -86,14 +92,43 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(private
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EventAnnotationsSummaryEntity.where(realm, event.eventId ?: "").findFirst()?.let {
|
EventAnnotationsSummaryEntity.where(realm, event.eventId
|
||||||
TimelineEventEntity.where(realm,eventId = event.eventId ?: "").findFirst()?.let { tet ->
|
?: "").findFirst()?.let {
|
||||||
|
TimelineEventEntity.where(realm, eventId = event.eventId
|
||||||
|
?: "").findFirst()?.let { tet ->
|
||||||
tet.annotations = it
|
tet.annotations = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EventType.ENCRYPTED -> {
|
||||||
|
//Relation type is in clear
|
||||||
|
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||||
|
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE) {
|
||||||
|
//we need to decrypt if needed
|
||||||
|
if (event.mxDecryptionResult == null) {
|
||||||
|
try {
|
||||||
|
val result = cryptoService.decryptEvent(event, event.roomId)
|
||||||
|
event.mxDecryptionResult = OlmDecryptionResult(
|
||||||
|
payload = result.clearEvent,
|
||||||
|
senderKey = result.senderCurve25519Key,
|
||||||
|
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||||
|
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
|
)
|
||||||
|
} catch (e: MXCryptoError) {
|
||||||
|
Timber.w("Failed to decrypt e2e replace")
|
||||||
|
//TODO -> we should keep track of this and retry, or aggregation will be broken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.getClearContent().toModel<MessageContent>()?.let {
|
||||||
|
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||||
|
//A replace!
|
||||||
|
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
EventType.REDACTION -> {
|
EventType.REDACTION -> {
|
||||||
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
||||||
?: return@forEach
|
?: return@forEach
|
||||||
|
@ -125,9 +160,9 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(private
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean) {
|
private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) {
|
||||||
val eventId = event.eventId ?: return
|
val eventId = event.eventId ?: return
|
||||||
val targetEventId = content.relatesTo?.eventId ?: return
|
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
|
||||||
val newContent = content.newContent ?: return
|
val newContent = content.newContent ?: return
|
||||||
//ok, this is a replace
|
//ok, this is a replace
|
||||||
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
|
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
|
||||||
|
|
|
@ -22,10 +22,11 @@ import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
|
import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
|
||||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
|
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
|
||||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
|
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsResponse
|
||||||
|
import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol
|
||||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
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.api.session.room.model.thirdparty.ThirdPartyProtocol
|
import im.vector.matrix.android.internal.session.room.relation.RelationsResponse
|
||||||
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
|
||||||
|
@ -195,6 +196,20 @@ internal interface RoomAPI {
|
||||||
@Body content: Content?
|
@Body content: Content?
|
||||||
): Call<SendResponse>
|
): Call<SendResponse>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginate relations for event based in normal topological order
|
||||||
|
*
|
||||||
|
* @param relationType filter for this relation type
|
||||||
|
* @param eventType filter for this event type
|
||||||
|
*/
|
||||||
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}/{eventType}")
|
||||||
|
fun getRelations(@Path("roomId") roomId: String,
|
||||||
|
@Path("eventId") eventId: String,
|
||||||
|
@Path("relationType") relationType: String,
|
||||||
|
@Path("eventType") eventType: String
|
||||||
|
): Call<RelationsResponse>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Join the given room.
|
* Join the given room.
|
||||||
*
|
*
|
||||||
|
|
|
@ -20,14 +20,13 @@ import com.zhuinden.monarchy.Monarchy
|
||||||
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.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.Membership
|
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomAvatarContent
|
import im.vector.matrix.android.api.session.room.model.RoomAvatarContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
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.RoomEntity
|
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.query.prev
|
import im.vector.matrix.android.internal.database.query.prev
|
||||||
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.SessionScope
|
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -42,32 +41,25 @@ internal class RoomAvatarResolver @Inject constructor(private val monarchy: Mona
|
||||||
fun resolve(roomId: String): String? {
|
fun resolve(roomId: String): String? {
|
||||||
var res: String? = null
|
var res: String? = null
|
||||||
monarchy.doWithRealm { realm ->
|
monarchy.doWithRealm { realm ->
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
|
||||||
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev()?.asDomain()
|
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev()?.asDomain()
|
||||||
res = roomName?.content.toModel<RoomAvatarContent>()?.avatarUrl
|
res = roomName?.content.toModel<RoomAvatarContent>()?.avatarUrl
|
||||||
if (!res.isNullOrEmpty()) {
|
if (!res.isNullOrEmpty()) {
|
||||||
return@doWithRealm
|
return@doWithRealm
|
||||||
}
|
}
|
||||||
val roomMembers = RoomMembers(realm, roomId)
|
val roomMembers = RoomMembers(realm, roomId)
|
||||||
val members = roomMembers.getLoaded()
|
val members = roomMembers.queryRoomMembersEvent().findAll()
|
||||||
if (roomEntity?.membership == Membership.INVITE) {
|
// detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat)
|
||||||
if (members.size == 1) {
|
if (members.size == 1) {
|
||||||
res = members.entries.first().value.avatarUrl
|
res = members.firstOrNull()?.toRoomMember()?.avatarUrl
|
||||||
} else if (members.size > 1) {
|
} else if (members.size == 2) {
|
||||||
val firstOtherMember = members.filterKeys { it != credentials.userId }.values.firstOrNull()
|
val firstOtherMember = members.where().notEqualTo(EventEntityFields.STATE_KEY, credentials.userId).findFirst()
|
||||||
res = firstOtherMember?.avatarUrl
|
res = firstOtherMember?.toRoomMember()?.avatarUrl
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat)
|
|
||||||
if (members.size == 1) {
|
|
||||||
res = members.entries.first().value.avatarUrl
|
|
||||||
} else if (members.size == 2) {
|
|
||||||
val firstOtherMember = members.filterKeys { it != credentials.userId }.values.firstOrNull()
|
|
||||||
res = firstOtherMember?.avatarUrl
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun EventEntity?.toRoomMember(): RoomMember? {
|
||||||
|
return this?.asDomain()?.content?.toModel<RoomMember>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,8 @@ import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRo
|
||||||
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.read.SetReadMarkersTask
|
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
||||||
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.relation.FetchEditHistoryTask
|
||||||
import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask
|
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.send.DefaultSendService
|
import im.vector.matrix.android.internal.session.room.send.DefaultSendService
|
||||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||||
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
|
import im.vector.matrix.android.internal.session.room.state.DefaultStateService
|
||||||
|
@ -56,7 +56,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
|
||||||
private val setReadMarkersTask: SetReadMarkersTask,
|
private val setReadMarkersTask: SetReadMarkersTask,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
||||||
private val updateQuickReactionTask: UpdateQuickReactionTask,
|
private val fetchEditHistoryTask: FetchEditHistoryTask,
|
||||||
private val joinRoomTask: JoinRoomTask,
|
private val joinRoomTask: JoinRoomTask,
|
||||||
private val leaveRoomTask: LeaveRoomTask) {
|
private val leaveRoomTask: LeaveRoomTask) {
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
|
||||||
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
|
val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask)
|
||||||
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials)
|
val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials)
|
||||||
val relationService = DefaultRelationService(context,
|
val relationService = DefaultRelationService(context,
|
||||||
credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, monarchy, taskExecutor)
|
credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor)
|
||||||
|
|
||||||
return DefaultRoom(
|
return DefaultRoom(
|
||||||
roomId,
|
roomId,
|
||||||
|
|
|
@ -142,4 +142,7 @@ internal abstract class RoomModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindFileService(fileService: DefaultFileService): FileService
|
abstract fun bindFileService(fileService: DefaultFileService): FileService
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindFetchEditHistoryTask(editHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
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.EventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.latestEvent
|
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||||
|
@ -86,12 +87,20 @@ internal class RoomSummaryUpdater @Inject constructor(private val credentials: C
|
||||||
|
|
||||||
val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includedTypes = PREVIEWABLE_TYPES)
|
val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, includedTypes = PREVIEWABLE_TYPES)
|
||||||
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()
|
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()
|
||||||
val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId }
|
|
||||||
|
val otherRoomMembers = RoomMembers(realm, roomId)
|
||||||
|
.queryRoomMembersEvent()
|
||||||
|
.notEqualTo(EventEntityFields.STATE_KEY, credentials.userId)
|
||||||
|
.findAll()
|
||||||
|
.asSequence()
|
||||||
|
.map { it.stateKey }
|
||||||
|
|
||||||
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
|
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
|
||||||
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
|
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
|
||||||
roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
|
roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
|
||||||
roomSummaryEntity.latestEvent = latestEvent
|
roomSummaryEntity.latestEvent = latestEvent
|
||||||
roomSummaryEntity.otherMemberIds.clear()
|
roomSummaryEntity.otherMemberIds.clear()
|
||||||
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers.keys)
|
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -17,9 +17,10 @@
|
||||||
package im.vector.matrix.android.internal.session.room.membership
|
package im.vector.matrix.android.internal.session.room.membership
|
||||||
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
|
import com.squareup.moshi.JsonReader
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.internal.database.helper.addStateEvents
|
import im.vector.matrix.android.internal.database.helper.addStateEvent
|
||||||
import im.vector.matrix.android.internal.database.helper.updateSenderData
|
import im.vector.matrix.android.internal.database.helper.updateSenderData
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
@ -27,10 +28,13 @@ import im.vector.matrix.android.internal.network.executeRequest
|
||||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||||
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
|
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
|
||||||
|
import im.vector.matrix.android.internal.session.user.UserEntityFactory
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import okio.Okio
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolean> {
|
internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolean> {
|
||||||
|
@ -60,23 +64,26 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun insertInDb(response: RoomMembersResponse, roomId: String): Try<RoomMembersResponse> {
|
private fun insertInDb(response: RoomMembersResponse, roomId: String): Try<Unit> {
|
||||||
return monarchy
|
return monarchy
|
||||||
.tryTransactionSync { realm ->
|
.tryTransactionSync { realm ->
|
||||||
// We ignore all the already known members
|
// We ignore all the already known members
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
val roomMembers = RoomMembers(realm, roomId).getLoaded()
|
|
||||||
val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) }
|
for (roomMemberEvent in response.roomMemberEvents) {
|
||||||
roomEntity.addStateEvents(eventsToInsert)
|
roomEntity.addStateEvent(roomMemberEvent)
|
||||||
|
UserEntityFactory.createOrNull(roomMemberEvent)?.also {
|
||||||
|
realm.insertOrUpdate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
roomEntity.chunks.flatMap { it.timelineEvents }.forEach {
|
roomEntity.chunks.flatMap { it.timelineEvents }.forEach {
|
||||||
it.updateSenderData()
|
it.updateSenderData()
|
||||||
}
|
}
|
||||||
roomEntity.areAllMembersLoaded = true
|
roomEntity.areAllMembersLoaded = true
|
||||||
roomSummaryUpdater.update(realm, roomId)
|
roomSummaryUpdater.update(realm, roomId)
|
||||||
}
|
}
|
||||||
.map { response }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun areAllMembersAlreadyLoaded(roomId: String): Boolean {
|
private fun areAllMembersAlreadyLoaded(roomId: String): Boolean {
|
||||||
|
@ -85,4 +92,4 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,13 +25,16 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
|
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
|
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
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.EventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.query.prev
|
import im.vector.matrix.android.internal.database.query.prev
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
import io.realm.RealmResults
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,7 +42,6 @@ import javax.inject.Inject
|
||||||
*/
|
*/
|
||||||
internal class RoomDisplayNameResolver @Inject constructor(private val context: Context,
|
internal class RoomDisplayNameResolver @Inject constructor(private val context: Context,
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
private val roomMemberDisplayNameResolver: RoomMemberDisplayNameResolver,
|
|
||||||
private val credentials: Credentials
|
private val credentials: Credentials
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -78,48 +80,61 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
|
||||||
}
|
}
|
||||||
|
|
||||||
val roomMembers = RoomMembers(realm, roomId)
|
val roomMembers = RoomMembers(realm, roomId)
|
||||||
val loadedMembers = roomMembers.getLoaded()
|
val loadedMembers = roomMembers.queryRoomMembersEvent().findAll()
|
||||||
val otherRoomMembers = loadedMembers.filterKeys { it != credentials.userId }
|
val otherMembersSubset = loadedMembers.where()
|
||||||
|
.notEqualTo(EventEntityFields.STATE_KEY, credentials.userId)
|
||||||
|
.limit(3)
|
||||||
|
.findAll()
|
||||||
|
|
||||||
if (roomEntity?.membership == Membership.INVITE) {
|
if (roomEntity?.membership == Membership.INVITE) {
|
||||||
val inviteMeEvent = roomMembers.queryRoomMemberEvent(credentials.userId).findFirst()
|
val inviteMeEvent = roomMembers.queryRoomMemberEvent(credentials.userId).findFirst()
|
||||||
val inviterId = inviteMeEvent?.sender
|
val inviterId = inviteMeEvent?.sender
|
||||||
name = if (inviterId != null && otherRoomMembers.containsKey(inviterId)) {
|
name = if (inviterId != null) {
|
||||||
roomMemberDisplayNameResolver.resolve(inviterId, otherRoomMembers)
|
val inviterMemberEvent = loadedMembers.where()
|
||||||
|
.equalTo(EventEntityFields.STATE_KEY, inviterId)
|
||||||
|
.findFirst()
|
||||||
|
inviterMemberEvent?.toRoomMember()?.displayName
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.room_displayname_room_invite)
|
context.getString(R.string.room_displayname_room_invite)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||||
val memberIds = if (roomSummary?.heroes?.isNotEmpty() == true) {
|
val memberIds: List<String> = if (roomSummary?.heroes?.isNotEmpty() == true) {
|
||||||
roomSummary.heroes
|
roomSummary.heroes
|
||||||
} else {
|
} else {
|
||||||
otherRoomMembers.keys.toList()
|
otherMembersSubset.mapNotNull { it.stateKey }
|
||||||
}
|
}
|
||||||
|
name = when (memberIds.size) {
|
||||||
val nbOfOtherMembers = memberIds.size
|
0 -> context.getString(R.string.room_displayname_empty_room)
|
||||||
|
1 -> resolveRoomMember(otherMembersSubset[0], roomMembers)
|
||||||
when (nbOfOtherMembers) {
|
2 -> context.getString(R.string.room_displayname_two_members,
|
||||||
0 -> name = context.getString(R.string.room_displayname_empty_room)
|
resolveRoomMember(otherMembersSubset[0], roomMembers),
|
||||||
1 -> name = roomMemberDisplayNameResolver.resolve(memberIds[0], otherRoomMembers)
|
resolveRoomMember(otherMembersSubset[1], roomMembers)
|
||||||
2 -> {
|
)
|
||||||
val member1 = memberIds[0]
|
else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
|
||||||
val member2 = memberIds[1]
|
roomMembers.getNumberOfJoinedMembers() - 1,
|
||||||
name = context.getString(R.string.room_displayname_two_members,
|
resolveRoomMember(otherMembersSubset[0], roomMembers),
|
||||||
roomMemberDisplayNameResolver.resolve(member1, otherRoomMembers),
|
roomMembers.getNumberOfJoinedMembers() - 1)
|
||||||
roomMemberDisplayNameResolver.resolve(member2, otherRoomMembers)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val member = memberIds[0]
|
|
||||||
name = context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
|
|
||||||
roomMembers.getNumberOfJoinedMembers() - 1,
|
|
||||||
roomMemberDisplayNameResolver.resolve(member, otherRoomMembers),
|
|
||||||
roomMembers.getNumberOfJoinedMembers() - 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@doWithRealm
|
return@doWithRealm
|
||||||
}
|
}
|
||||||
return name ?: roomId
|
return name ?: roomId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveRoomMember(eventEntity: EventEntity?,
|
||||||
|
roomMembers: RoomMembers): String? {
|
||||||
|
if (eventEntity == null) return null
|
||||||
|
val roomMember = eventEntity.toRoomMember() ?: return null
|
||||||
|
val isUnique = roomMembers.isUniqueDisplayName(roomMember.displayName)
|
||||||
|
return if (isUnique) {
|
||||||
|
roomMember.displayName
|
||||||
|
} else {
|
||||||
|
"${roomMember.displayName} (${eventEntity.stateKey})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun EventEntity?.toRoomMember(): RoomMember? {
|
||||||
|
return this?.asDomain()?.content?.toModel<RoomMember>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +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.matrix.android.internal.session.room.membership
|
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class RoomMemberDisplayNameResolver @Inject constructor() {
|
|
||||||
|
|
||||||
fun resolve(userId: String, members: Map<String, RoomMember>): String? {
|
|
||||||
val currentMember = members[userId]
|
|
||||||
var displayName = currentMember?.displayName
|
|
||||||
// Get the user display name from the member list of the room
|
|
||||||
// Do not consider null display name
|
|
||||||
|
|
||||||
if (currentMember != null && !currentMember.displayName.isNullOrEmpty()) {
|
|
||||||
val hasNameCollision = members
|
|
||||||
.filterValues { it != currentMember && it.displayName == currentMember.displayName }
|
|
||||||
.isNotEmpty()
|
|
||||||
if (hasNameCollision) {
|
|
||||||
displayName = "${currentMember.displayName} ( $userId )"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO handle invited users
|
|
||||||
/*else if (null != member && TextUtils.equals(member!!.membership, RoomMember.MEMBERSHIP_INVITE)) {
|
|
||||||
val user = (mDataHandler as MXDataHandler).getUser(userId)
|
|
||||||
if (null != user) {
|
|
||||||
displayName = user!!.displayname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (displayName == null) {
|
|
||||||
// By default, use the user ID
|
|
||||||
displayName = userId
|
|
||||||
}
|
|
||||||
return displayName
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -33,6 +33,7 @@ import io.realm.Sort
|
||||||
* This class is an helper around STATE_ROOM_MEMBER events.
|
* This class is an helper around STATE_ROOM_MEMBER events.
|
||||||
* It allows to get the live membership of a user.
|
* It allows to get the live membership of a user.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
internal class RoomMembers(private val realm: Realm,
|
internal class RoomMembers(private val realm: Realm,
|
||||||
private val roomId: String
|
private val roomId: String
|
||||||
) {
|
) {
|
||||||
|
@ -72,27 +73,27 @@ internal class RoomMembers(private val realm: Realm,
|
||||||
.isNotNull(EventEntityFields.CONTENT)
|
.isNotNull(EventEntityFields.CONTENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun queryJoinedRoomMembersEvent(): RealmQuery<EventEntity> {
|
||||||
|
return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"join\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryInvitedRoomMembersEvent(): RealmQuery<EventEntity> {
|
||||||
|
return queryRoomMembersEvent().contains(EventEntityFields.CONTENT, "\"membership\":\"invite\"")
|
||||||
|
}
|
||||||
|
|
||||||
fun queryRoomMemberEvent(userId: String): RealmQuery<EventEntity> {
|
fun queryRoomMemberEvent(userId: String): RealmQuery<EventEntity> {
|
||||||
return queryRoomMembersEvent()
|
return queryRoomMembersEvent()
|
||||||
.equalTo(EventEntityFields.STATE_KEY, userId)
|
.equalTo(EventEntityFields.STATE_KEY, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLoaded(): Map<String, RoomMember> {
|
|
||||||
return queryRoomMembersEvent()
|
|
||||||
.findAll()
|
|
||||||
.map { it.asDomain() }
|
|
||||||
.associateBy { it.stateKey!! }
|
|
||||||
.mapValues { it.value.content.toModel<RoomMember>()!! }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getNumberOfJoinedMembers(): Int {
|
fun getNumberOfJoinedMembers(): Int {
|
||||||
return roomSummary?.joinedMembersCount
|
return roomSummary?.joinedMembersCount
|
||||||
?: getLoaded().filterValues { it.membership == Membership.JOIN }.size
|
?: queryJoinedRoomMembersEvent().findAll().size
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNumberOfInvitedMembers(): Int {
|
fun getNumberOfInvitedMembers(): Int {
|
||||||
return roomSummary?.invitedMembersCount
|
return roomSummary?.invitedMembersCount
|
||||||
?: getLoaded().filterValues { it.membership == Membership.INVITE }.size
|
?: queryInvitedRoomMembersEvent().findAll().size
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNumberOfMembers(): Int {
|
fun getNumberOfMembers(): Int {
|
||||||
|
@ -133,4 +134,4 @@ internal class RoomMembers(private val realm: Realm,
|
||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,13 +61,13 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
|
||||||
}
|
}
|
||||||
|
|
||||||
val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId
|
val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId
|
||||||
?: "").findFirst()
|
?: "").findFirst()
|
||||||
?: return
|
?: return
|
||||||
val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT
|
val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT
|
||||||
Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho")
|
Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho")
|
||||||
|
|
||||||
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
|
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
val allowedKeys = computeAllowedKeys(eventToPrune.type)
|
val allowedKeys = computeAllowedKeys(eventToPrune.type)
|
||||||
if (allowedKeys.isNotEmpty()) {
|
if (allowedKeys.isNotEmpty()) {
|
||||||
|
@ -75,10 +75,11 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
|
||||||
eventToPrune.content = ContentMapper.map(prunedContent)
|
eventToPrune.content = ContentMapper.map(prunedContent)
|
||||||
} else {
|
} else {
|
||||||
when (eventToPrune.type) {
|
when (eventToPrune.type) {
|
||||||
|
EventType.ENCRYPTED,
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE -> {
|
||||||
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||||
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||||
?: UnsignedData(null, null)
|
?: UnsignedData(null, null)
|
||||||
|
|
||||||
//was this event a m.replace
|
//was this event a m.replace
|
||||||
// val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
// val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
||||||
|
@ -89,6 +90,8 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
|
||||||
val modified = unsignedData.copy(redactedEvent = redactionEvent)
|
val modified = unsignedData.copy(redactedEvent = redactionEvent)
|
||||||
eventToPrune.content = ContentMapper.map(emptyMap())
|
eventToPrune.content = ContentMapper.map(emptyMap())
|
||||||
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
|
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
|
||||||
|
eventToPrune.decryptionResultJson = null
|
||||||
|
eventToPrune.decryptionErrorCode = null
|
||||||
|
|
||||||
}
|
}
|
||||||
// EventType.REACTION -> {
|
// EventType.REACTION -> {
|
||||||
|
@ -112,14 +115,14 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
|
||||||
EventType.STATE_ROOM_CREATE -> listOf("creator")
|
EventType.STATE_ROOM_CREATE -> listOf("creator")
|
||||||
EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule")
|
EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule")
|
||||||
EventType.STATE_ROOM_POWER_LEVELS -> listOf("users",
|
EventType.STATE_ROOM_POWER_LEVELS -> listOf("users",
|
||||||
"users_default",
|
"users_default",
|
||||||
"events",
|
"events",
|
||||||
"events_default",
|
"events_default",
|
||||||
"state_default",
|
"state_default",
|
||||||
"ban",
|
"ban",
|
||||||
"kick",
|
"kick",
|
||||||
"redact",
|
"redact",
|
||||||
"invite")
|
"invite")
|
||||||
EventType.STATE_ROOM_ALIASES -> listOf("aliases")
|
EventType.STATE_ROOM_ALIASES -> listOf("aliases")
|
||||||
EventType.STATE_CANONICAL_ALIAS -> listOf("alias")
|
EventType.STATE_CANONICAL_ALIAS -> listOf("alias")
|
||||||
EventType.FEEDBACK -> listOf("type", "target_event_id")
|
EventType.FEEDBACK -> listOf("type", "target_event_id")
|
||||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
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.room.model.EventAnnotationsSummary
|
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||||
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.util.Cancelable
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
@ -53,6 +54,7 @@ internal class DefaultRelationService @Inject constructor(private val context: C
|
||||||
private val eventFactory: LocalEchoEventFactory,
|
private val eventFactory: LocalEchoEventFactory,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
|
||||||
|
private val fetchEditHistoryTask: FetchEditHistoryTask,
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
private val taskExecutor: TaskExecutor)
|
private val taskExecutor: TaskExecutor)
|
||||||
: RelationService {
|
: RelationService {
|
||||||
|
@ -125,10 +127,50 @@ internal class DefaultRelationService @Inject constructor(private val context: C
|
||||||
.also {
|
.also {
|
||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
}
|
}
|
||||||
val workRequest = createSendEventWork(event)
|
if (cryptoService.isRoomEncrypted(roomId)) {
|
||||||
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
|
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
|
||||||
return CancelableWork(context, workRequest.id)
|
val workRequest = createSendEventWork(event)
|
||||||
|
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
|
||||||
|
return CancelableWork(context, encryptWork.id)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
val workRequest = createSendEventWork(event)
|
||||||
|
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
|
||||||
|
return CancelableWork(context, workRequest.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun editReply(replyToEdit: TimelineEvent,
|
||||||
|
originalEvent: TimelineEvent,
|
||||||
|
newBodyText: String,
|
||||||
|
compatibilityBodyText: String): Cancelable {
|
||||||
|
val event = eventFactory
|
||||||
|
.createReplaceTextOfReply(roomId,
|
||||||
|
replyToEdit,
|
||||||
|
originalEvent,
|
||||||
|
newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
|
||||||
|
.also {
|
||||||
|
saveLocalEcho(it)
|
||||||
|
}
|
||||||
|
if (cryptoService.isRoomEncrypted(roomId)) {
|
||||||
|
val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
|
||||||
|
val workRequest = createSendEventWork(event)
|
||||||
|
TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest)
|
||||||
|
return CancelableWork(context, encryptWork.id)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
val workRequest = createSendEventWork(event)
|
||||||
|
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
|
||||||
|
return CancelableWork(context, workRequest.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
|
||||||
|
val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId)
|
||||||
|
fetchEditHistoryTask.configureWith(params)
|
||||||
|
.dispatchTo(callback)
|
||||||
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? {
|
override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? {
|
||||||
|
@ -169,7 +211,8 @@ internal class DefaultRelationService @Inject constructor(private val context: C
|
||||||
EventAnnotationsSummaryEntity.where(realm, eventId)
|
EventAnnotationsSummaryEntity.where(realm, eventId)
|
||||||
}
|
}
|
||||||
return Transformations.map(liveEntity) { realmResults ->
|
return Transformations.map(liveEntity) { realmResults ->
|
||||||
realmResults.firstOrNull()?.asDomain() ?: EventAnnotationsSummary(eventId, emptyList(), null)
|
realmResults.firstOrNull()?.asDomain()
|
||||||
|
?: EventAnnotationsSummary(eventId, emptyList(), null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* 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.relation
|
||||||
|
|
||||||
|
import arrow.core.Try
|
||||||
|
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.RelationType
|
||||||
|
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 FetchEditHistoryTask : Task<FetchEditHistoryTask.Params, List<Event>> {
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val roomId: String,
|
||||||
|
val isRoomEncrypted: Boolean,
|
||||||
|
val eventId: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal class DefaultFetchEditHistoryTask @Inject constructor(
|
||||||
|
private val roomAPI: RoomAPI
|
||||||
|
) : FetchEditHistoryTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: FetchEditHistoryTask.Params): Try<List<Event>> {
|
||||||
|
return executeRequest<RelationsResponse> {
|
||||||
|
apiCall = roomAPI.getRelations(params.roomId,
|
||||||
|
params.eventId,
|
||||||
|
RelationType.REPLACE,
|
||||||
|
if (params.isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE)
|
||||||
|
}.map { resp ->
|
||||||
|
val events = resp.chunks.toMutableList()
|
||||||
|
resp.originalEvent?.let { events.add(it) }
|
||||||
|
events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,9 +13,16 @@
|
||||||
* 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.matrix.android.internal.session.room.relation
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
|
||||||
import im.vector.riotx.core.utils.RxStore
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class RelationsResponse(
|
||||||
class VisibleRoomStore : RxStore<String>()
|
@Json(name = "chunk") val chunks: List<Event>,
|
||||||
|
@Json(name = "original_event") val originalEvent: Event?,
|
||||||
|
@Json(name = "next_batch") val nextBatch: String?,
|
||||||
|
@Json(name = "prev_batch") val prevBatch: String?
|
||||||
|
)
|
|
@ -104,6 +104,45 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createReplaceTextOfReply(roomId: String, eventReplaced: TimelineEvent,
|
||||||
|
originalEvent: TimelineEvent,
|
||||||
|
newBodyText: String,
|
||||||
|
newBodyAutoMarkdown: Boolean,
|
||||||
|
msgType: String,
|
||||||
|
compatibilityText: String): Event {
|
||||||
|
val permalink = PermalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "")
|
||||||
|
val userLink = originalEvent.root.senderId?.let { PermalinkFactory.createPermalink(it) }
|
||||||
|
?: ""
|
||||||
|
|
||||||
|
val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.root.getClearContent().toModel())
|
||||||
|
val replyFormatted = REPLY_PATTERN.format(
|
||||||
|
permalink,
|
||||||
|
stringProvider.getString(R.string.message_reply_to_prefix),
|
||||||
|
userLink,
|
||||||
|
originalEvent.senderName ?: originalEvent.root.senderId,
|
||||||
|
body.takeFormatted(),
|
||||||
|
createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
|
||||||
|
)
|
||||||
|
//
|
||||||
|
// > <@alice:example.org> This is the original body
|
||||||
|
//
|
||||||
|
val replyFallback = buildReplyFallback(body, originalEvent.root.senderId ?: "", newBodyText)
|
||||||
|
|
||||||
|
return createEvent(roomId,
|
||||||
|
MessageTextContent(
|
||||||
|
type = msgType,
|
||||||
|
body = compatibilityText,
|
||||||
|
relatesTo = RelationDefaultContent(RelationType.REPLACE, eventReplaced.root.eventId),
|
||||||
|
newContent = MessageTextContent(
|
||||||
|
type = msgType,
|
||||||
|
format = MessageType.FORMAT_MATRIX_HTML,
|
||||||
|
body = replyFallback,
|
||||||
|
formattedBody = replyFormatted
|
||||||
|
)
|
||||||
|
.toContent()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event {
|
||||||
return when (attachment.type) {
|
return when (attachment.type) {
|
||||||
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment)
|
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment)
|
||||||
|
@ -239,16 +278,8 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||||
val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null
|
val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null
|
||||||
val userId = eventReplied.root.senderId ?: return null
|
val userId = eventReplied.root.senderId ?: return null
|
||||||
val userLink = PermalinkFactory.createPermalink(userId) ?: return null
|
val userLink = PermalinkFactory.createPermalink(userId) ?: return null
|
||||||
// <mx-reply>
|
|
||||||
// <blockquote>
|
val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.root.getClearContent().toModel())
|
||||||
// <a href="https://matrix.to/#/!somewhere:domain.com/$event:domain.com">In reply to</a>
|
|
||||||
// <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a>
|
|
||||||
// <br />
|
|
||||||
// <!-- This is where the related event's HTML would be. -->
|
|
||||||
// </blockquote>
|
|
||||||
// </mx-reply>
|
|
||||||
// This is where the reply goes.
|
|
||||||
val body = bodyForReply(eventReplied.getLastMessageContent())
|
|
||||||
val replyFormatted = REPLY_PATTERN.format(
|
val replyFormatted = REPLY_PATTERN.format(
|
||||||
permalink,
|
permalink,
|
||||||
stringProvider.getString(R.string.message_reply_to_prefix),
|
stringProvider.getString(R.string.message_reply_to_prefix),
|
||||||
|
@ -260,8 +291,22 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||||
//
|
//
|
||||||
// > <@alice:example.org> This is the original body
|
// > <@alice:example.org> This is the original body
|
||||||
//
|
//
|
||||||
|
val replyFallback = buildReplyFallback(body, userId, replyText)
|
||||||
|
|
||||||
|
val eventId = eventReplied.root.eventId ?: return null
|
||||||
|
val content = MessageTextContent(
|
||||||
|
type = MessageType.MSGTYPE_TEXT,
|
||||||
|
format = MessageType.FORMAT_MATRIX_HTML,
|
||||||
|
body = replyFallback,
|
||||||
|
formattedBody = replyFormatted,
|
||||||
|
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
|
||||||
|
)
|
||||||
|
return createEvent(roomId, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {
|
||||||
val lines = body.text.split("\n")
|
val lines = body.text.split("\n")
|
||||||
val replyFallback = StringBuffer("><$userId>")
|
val replyFallback = StringBuffer("><$originalSenderId>")
|
||||||
lines.forEachIndexed { index, s ->
|
lines.forEachIndexed { index, s ->
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
replyFallback.append(" $s")
|
replyFallback.append(" $s")
|
||||||
|
@ -269,23 +314,16 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||||
replyFallback.append("\n>$s")
|
replyFallback.append("\n>$s")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
replyFallback.append("\n\n").append(replyText)
|
replyFallback.append("\n\n").append(newBodyText)
|
||||||
|
return replyFallback.toString()
|
||||||
val eventId = eventReplied.root.eventId ?: return null
|
|
||||||
val content = MessageTextContent(
|
|
||||||
type = MessageType.MSGTYPE_TEXT,
|
|
||||||
format = MessageType.FORMAT_MATRIX_HTML,
|
|
||||||
body = replyFallback.toString(),
|
|
||||||
formattedBody = replyFormatted,
|
|
||||||
relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId))
|
|
||||||
)
|
|
||||||
return createEvent(roomId, content)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a TextContent used for the fallback event representation in a reply message.
|
* Returns a TextContent used for the fallback event representation in a reply message.
|
||||||
|
* We also pass the original content, because in case of an edit of a reply the last content is not
|
||||||
|
* himself a reply, but it will contain the fallbacks, so we have to trim them.
|
||||||
*/
|
*/
|
||||||
private fun bodyForReply(content: MessageContent?): TextContent {
|
private fun bodyForReply(content: MessageContent?, originalContent: MessageContent?): TextContent {
|
||||||
when (content?.type) {
|
when (content?.type) {
|
||||||
MessageType.MSGTYPE_EMOTE,
|
MessageType.MSGTYPE_EMOTE,
|
||||||
MessageType.MSGTYPE_TEXT,
|
MessageType.MSGTYPE_TEXT,
|
||||||
|
@ -296,7 +334,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||||
formattedText = content.formattedBody
|
formattedText = content.formattedBody
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val isReply = content.relatesTo?.inReplyTo?.eventId != null
|
val isReply = content.isReply() || originalContent.isReply()
|
||||||
return if (isReply)
|
return if (isReply)
|
||||||
TextContent(content.body, formattedText).removeInReplyFallbacks()
|
TextContent(content.body, formattedText).removeInReplyFallbacks()
|
||||||
else
|
else
|
||||||
|
@ -353,7 +391,16 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
|
||||||
companion object {
|
companion object {
|
||||||
const val LOCAL_ID_PREFIX = "local."
|
const val LOCAL_ID_PREFIX = "local."
|
||||||
|
|
||||||
// No whitespace
|
|
||||||
|
// <mx-reply>
|
||||||
|
// <blockquote>
|
||||||
|
// <a href="https://matrix.to/#/!somewhere:domain.com/$event:domain.com">In reply to</a>
|
||||||
|
// <a href="https://matrix.to/#/@alice:example.org">@alice:example.org</a>
|
||||||
|
// <br />
|
||||||
|
// <!-- This is where the related event's HTML would be. -->
|
||||||
|
// </blockquote>
|
||||||
|
// </mx-reply>
|
||||||
|
// No whitespace because currently breaks temporary formatted text to Span
|
||||||
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">%s</a><a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
|
const val REPLY_PATTERN = """<mx-reply><blockquote><a href="%s">%s</a><a href="%s">%s</a><br />%s</blockquote></mx-reply>%s"""
|
||||||
|
|
||||||
fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(LOCAL_ID_PREFIX)
|
fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(LOCAL_ID_PREFIX)
|
||||||
|
|
|
@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.session.room.send
|
||||||
|
|
||||||
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.util.ContentUtils.extractUsefulTextFromHtmlReply
|
||||||
|
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains a text and eventually a formatted text
|
* Contains a text and eventually a formatted text
|
||||||
|
@ -47,28 +49,4 @@ fun TextContent.removeInReplyFallbacks(): TextContent {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractUsefulTextFromReply(repliedBody: String): String {
|
|
||||||
val lines = repliedBody.lines()
|
|
||||||
var wellFormed = repliedBody.startsWith(">")
|
|
||||||
var endOfPreviousFound = false
|
|
||||||
val usefullines = ArrayList<String>()
|
|
||||||
lines.forEach {
|
|
||||||
if (it == "") {
|
|
||||||
endOfPreviousFound = true
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
if (!endOfPreviousFound) {
|
|
||||||
wellFormed = wellFormed && it.startsWith(">")
|
|
||||||
} else {
|
|
||||||
usefullines.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
|
|
||||||
if (repliedBody.startsWith("<mx-reply>")) {
|
|
||||||
return repliedBody.substring(repliedBody.lastIndexOf("</mx-reply>") + "</mx-reply>".length).trim()
|
|
||||||
}
|
|
||||||
return repliedBody
|
|
||||||
}
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
|
|
||||||
private const val INITIAL_LOAD_SIZE = 10
|
private const val INITIAL_LOAD_SIZE = 30
|
||||||
private const val MIN_FETCHING_COUNT = 30
|
private const val MIN_FETCHING_COUNT = 30
|
||||||
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
|
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
|
||||||
|
|
||||||
|
|
|
@ -18,18 +18,14 @@ package im.vector.matrix.android.internal.session.room.timeline
|
||||||
|
|
||||||
import arrow.core.Try
|
import arrow.core.Try
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.internal.database.helper.addAll
|
import im.vector.matrix.android.internal.database.helper.*
|
||||||
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
|
||||||
import im.vector.matrix.android.internal.database.helper.addStateEvents
|
|
||||||
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
|
||||||
import im.vector.matrix.android.internal.database.helper.isUnlinked
|
|
||||||
import im.vector.matrix.android.internal.database.helper.merge
|
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.query.create
|
import im.vector.matrix.android.internal.database.query.create
|
||||||
import im.vector.matrix.android.internal.database.query.find
|
import im.vector.matrix.android.internal.database.query.find
|
||||||
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
||||||
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.user.UserEntityFactory
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -117,7 +113,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||||
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
||||||
|
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
val nextToken: String?
|
val nextToken: String?
|
||||||
val prevToken: String?
|
val prevToken: String?
|
||||||
|
@ -146,15 +142,21 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||||
} else {
|
} else {
|
||||||
nextChunk?.apply { this.prevToken = prevToken }
|
nextChunk?.apply { this.prevToken = prevToken }
|
||||||
}
|
}
|
||||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||||
|
|
||||||
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
||||||
Timber.v("Reach end of $roomId")
|
Timber.v("Reach end of $roomId")
|
||||||
currentChunk.isLastBackward = true
|
currentChunk.isLastBackward = true
|
||||||
} else {
|
} else {
|
||||||
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
||||||
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
val eventIds = ArrayList<String>(receivedChunk.events.size)
|
||||||
|
for (event in receivedChunk.events) {
|
||||||
|
event.eventId?.also { eventIds.add(it) }
|
||||||
|
currentChunk.add(roomId, event, direction, isUnlinked = currentChunk.isUnlinked())
|
||||||
|
UserEntityFactory.createOrNull(event)?.also {
|
||||||
|
realm.insertOrUpdate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Then we merge chunks if needed
|
// Then we merge chunks if needed
|
||||||
if (currentChunk != prevChunk && prevChunk != null) {
|
if (currentChunk != prevChunk && prevChunk != null) {
|
||||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
||||||
|
@ -170,7 +172,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomEntity.addOrUpdate(currentChunk)
|
roomEntity.addOrUpdate(currentChunk)
|
||||||
roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = currentChunk.isUnlinked())
|
for (stateEvent in receivedChunk.stateEvents) {
|
||||||
|
roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked())
|
||||||
|
UserEntityFactory.createOrNull(stateEvent)?.also {
|
||||||
|
realm.insertOrUpdate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentChunk.updateSenderDataFor(eventIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map {
|
.map {
|
||||||
|
|
|
@ -24,12 +24,11 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
|
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
|
||||||
import im.vector.matrix.android.internal.crypto.CryptoManager
|
import im.vector.matrix.android.internal.crypto.CryptoManager
|
||||||
import im.vector.matrix.android.internal.database.helper.addAll
|
import im.vector.matrix.android.internal.database.helper.*
|
||||||
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
|
||||||
import im.vector.matrix.android.internal.database.helper.addStateEvents
|
|
||||||
import im.vector.matrix.android.internal.database.helper.lastStateIndex
|
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||||
|
import im.vector.matrix.android.internal.database.model.UserEntity
|
||||||
import im.vector.matrix.android.internal.database.query.find
|
import im.vector.matrix.android.internal.database.query.find
|
||||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
@ -40,6 +39,7 @@ import im.vector.matrix.android.internal.session.notification.ProcessEventForPus
|
||||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||||
import im.vector.matrix.android.internal.session.sync.model.*
|
import im.vector.matrix.android.internal.session.sync.model.*
|
||||||
|
import im.vector.matrix.android.internal.session.user.UserEntityFactory
|
||||||
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
|
||||||
|
@ -125,51 +125,31 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
}
|
}
|
||||||
roomEntity.membership = Membership.JOIN
|
roomEntity.membership = Membership.JOIN
|
||||||
|
|
||||||
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
|
||||||
val isInitialSync = lastChunk == null
|
|
||||||
val lastStateIndex = lastChunk?.lastStateIndex(PaginationDirection.FORWARDS) ?: 0
|
|
||||||
val numberOfStateEvents = roomSync.state?.events?.size ?: 0
|
|
||||||
val stateIndexOffset = lastStateIndex + numberOfStateEvents
|
|
||||||
|
|
||||||
// State event
|
// State event
|
||||||
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
|
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
|
||||||
val untimelinedStateIndex = if (isInitialSync) Int.MIN_VALUE else stateIndexOffset
|
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
|
||||||
roomEntity.addStateEvents(roomSync.state.events, filterDuplicates = true, stateIndex = untimelinedStateIndex)
|
?: Int.MIN_VALUE
|
||||||
|
val untimelinedStateIndex = minStateIndex + 1
|
||||||
// Give info to crypto module
|
roomSync.state.events.forEach { event ->
|
||||||
roomSync.state.events.forEach {
|
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
|
||||||
cryptoManager.onStateEvent(roomId, it)
|
// Give info to crypto module
|
||||||
|
cryptoManager.onStateEvent(roomId, event)
|
||||||
|
UserEntityFactory.createOrNull(event)?.also {
|
||||||
|
realm.insertOrUpdate(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
|
if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
|
||||||
val timelineStateOffset = if (isInitialSync || roomSync.timeline.limited.not()) 0 else stateIndexOffset
|
|
||||||
val chunkEntity = handleTimelineEvents(
|
val chunkEntity = handleTimelineEvents(
|
||||||
realm,
|
realm,
|
||||||
roomId,
|
roomEntity,
|
||||||
roomSync.timeline.events,
|
roomSync.timeline.events,
|
||||||
roomSync.timeline.prevToken,
|
roomSync.timeline.prevToken,
|
||||||
roomSync.timeline.limited,
|
roomSync.timeline.limited,
|
||||||
timelineStateOffset
|
0
|
||||||
)
|
)
|
||||||
roomEntity.addOrUpdate(chunkEntity)
|
roomEntity.addOrUpdate(chunkEntity)
|
||||||
|
|
||||||
// Give info to crypto module
|
|
||||||
roomSync.timeline.events.forEach {
|
|
||||||
cryptoManager.onLiveEvent(roomId, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to remove local echo
|
|
||||||
val transactionIds = roomSync.timeline.events.mapNotNull { it.unsignedData?.transactionId }
|
|
||||||
transactionIds.forEach {
|
|
||||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
|
||||||
if (sendingEventEntity != null) {
|
|
||||||
Timber.v("Remove local echo for tx:$it")
|
|
||||||
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
|
|
||||||
} else {
|
|
||||||
Timber.v("Can't find corresponding local echo for tx:$it")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)
|
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)
|
||||||
|
|
||||||
|
@ -192,7 +172,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
roomEntity.membership = Membership.INVITE
|
roomEntity.membership = Membership.INVITE
|
||||||
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
|
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
|
||||||
val chunkEntity = handleTimelineEvents(realm, roomId, roomSync.inviteState.events)
|
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
|
||||||
roomEntity.addOrUpdate(chunkEntity)
|
roomEntity.addOrUpdate(chunkEntity)
|
||||||
}
|
}
|
||||||
roomSummaryUpdater.update(realm, roomId, Membership.INVITE)
|
roomSummaryUpdater.update(realm, roomId, Membership.INVITE)
|
||||||
|
@ -212,13 +192,13 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleTimelineEvents(realm: Realm,
|
private fun handleTimelineEvents(realm: Realm,
|
||||||
roomId: String,
|
roomEntity: RoomEntity,
|
||||||
eventList: List<Event>,
|
eventList: List<Event>,
|
||||||
prevToken: String? = null,
|
prevToken: String? = null,
|
||||||
isLimited: Boolean = true,
|
isLimited: Boolean = true,
|
||||||
stateIndexOffset: Int = 0): ChunkEntity {
|
stateIndexOffset: Int = 0): ChunkEntity {
|
||||||
|
|
||||||
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
|
||||||
val chunkEntity = if (!isLimited && lastChunk != null) {
|
val chunkEntity = if (!isLimited && lastChunk != null) {
|
||||||
lastChunk
|
lastChunk
|
||||||
} else {
|
} else {
|
||||||
|
@ -226,13 +206,32 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||||
}
|
}
|
||||||
lastChunk?.isLastForward = false
|
lastChunk?.isLastForward = false
|
||||||
chunkEntity.isLastForward = true
|
chunkEntity.isLastForward = true
|
||||||
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)
|
|
||||||
|
|
||||||
//update eventAnnotationSummary here?
|
|
||||||
|
|
||||||
|
val eventIds = ArrayList<String>(eventList.size)
|
||||||
|
for (event in eventList) {
|
||||||
|
event.eventId?.also { eventIds.add(it) }
|
||||||
|
chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)
|
||||||
|
// Give info to crypto module
|
||||||
|
cryptoManager.onLiveEvent(roomEntity.roomId, event)
|
||||||
|
// Try to remove local echo
|
||||||
|
event.unsignedData?.transactionId?.also {
|
||||||
|
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
||||||
|
if (sendingEventEntity != null) {
|
||||||
|
Timber.v("Remove local echo for tx:$it")
|
||||||
|
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
|
||||||
|
} else {
|
||||||
|
Timber.v("Can't find corresponding local echo for tx:$it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserEntityFactory.createOrNull(event)?.also {
|
||||||
|
realm.insertOrUpdate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunkEntity.updateSenderDataFor(eventIds)
|
||||||
return chunkEntity
|
return chunkEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun handleEphemeral(realm: Realm,
|
private fun handleEphemeral(realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
ephemeral: RoomSyncEphemeral) {
|
ephemeral: RoomSyncEphemeral) {
|
||||||
|
|
|
@ -20,8 +20,6 @@ import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import im.vector.matrix.android.internal.session.user.DefaultUpdateUserTask
|
|
||||||
import im.vector.matrix.android.internal.session.user.UpdateUserTask
|
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
|
|
|
@ -37,7 +37,6 @@ import javax.inject.Inject
|
||||||
|
|
||||||
private const val RETRY_WAIT_TIME_MS = 10_000L
|
private const val RETRY_WAIT_TIME_MS = 10_000L
|
||||||
private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L
|
private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L
|
||||||
private const val DEFAULT_LONG_POOL_DELAY = 0L
|
|
||||||
|
|
||||||
internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
private val networkConnectivityChecker: NetworkConnectivityChecker,
|
private val networkConnectivityChecker: NetworkConnectivityChecker,
|
||||||
|
@ -54,10 +53,15 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
updateStateTo(SyncState.IDLE)
|
updateStateTo(SyncState.IDLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setInitialForeground(initialForeground: Boolean) {
|
||||||
|
val newState = if (initialForeground) SyncState.IDLE else SyncState.PAUSED
|
||||||
|
updateStateTo(newState)
|
||||||
|
}
|
||||||
|
|
||||||
fun restart() = synchronized(lock) {
|
fun restart() = synchronized(lock) {
|
||||||
if (state is SyncState.PAUSED) {
|
if (state is SyncState.PAUSED) {
|
||||||
Timber.v("Resume sync...")
|
Timber.v("Resume sync...")
|
||||||
updateStateTo(SyncState.RUNNING(catchingUp = true))
|
updateStateTo(SyncState.RUNNING(afterPause = true))
|
||||||
lock.notify()
|
lock.notify()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,7 +88,6 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
Timber.v("Start syncing...")
|
Timber.v("Start syncing...")
|
||||||
networkConnectivityChecker.register(this)
|
networkConnectivityChecker.register(this)
|
||||||
backgroundDetectionObserver.register(this)
|
backgroundDetectionObserver.register(this)
|
||||||
updateStateTo(SyncState.RUNNING(catchingUp = true))
|
|
||||||
|
|
||||||
while (state != SyncState.KILLING) {
|
while (state != SyncState.KILLING) {
|
||||||
if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) {
|
if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) {
|
||||||
|
@ -93,7 +96,10 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
lock.wait()
|
lock.wait()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.v("Execute sync request with timeout $DEFAULT_LONG_POOL_TIMEOUT")
|
if (state !is SyncState.RUNNING) {
|
||||||
|
updateStateTo(SyncState.RUNNING(afterPause = true))
|
||||||
|
}
|
||||||
|
Timber.v("[$this] Execute sync request with timeout $DEFAULT_LONG_POOL_TIMEOUT")
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
val params = SyncTask.Params(DEFAULT_LONG_POOL_TIMEOUT)
|
val params = SyncTask.Params(DEFAULT_LONG_POOL_TIMEOUT)
|
||||||
cancelableTask = syncTask.configureWith(params)
|
cancelableTask = syncTask.configureWith(params)
|
||||||
|
@ -133,11 +139,9 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
|
|
||||||
latch.await()
|
latch.await()
|
||||||
if (state is SyncState.RUNNING) {
|
if (state is SyncState.RUNNING) {
|
||||||
updateStateTo(SyncState.RUNNING(catchingUp = false))
|
updateStateTo(SyncState.RUNNING(afterPause = false))
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.v("Waiting for $DEFAULT_LONG_POOL_DELAY delay before new pool...")
|
|
||||||
if (DEFAULT_LONG_POOL_DELAY > 0) sleep(DEFAULT_LONG_POOL_DELAY)
|
|
||||||
Timber.v("...Continue")
|
Timber.v("...Continue")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,6 +152,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStateTo(newState: SyncState) {
|
private fun updateStateTo(newState: SyncState) {
|
||||||
|
Timber.v("Update state to $newState")
|
||||||
state = newState
|
state = newState
|
||||||
liveState.postValue(newState)
|
liveState.postValue(newState)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,58 +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.matrix.android.internal.session.user
|
|
||||||
|
|
||||||
import arrow.core.Try
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
|
||||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.UserEntity
|
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
|
||||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
|
||||||
import im.vector.matrix.android.internal.task.Task
|
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal interface UpdateUserTask : Task<UpdateUserTask.Params, Unit> {
|
|
||||||
|
|
||||||
data class Params(val eventIds: List<String>)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class DefaultUpdateUserTask @Inject constructor(private val monarchy: Monarchy) : UpdateUserTask {
|
|
||||||
|
|
||||||
override suspend fun execute(params: UpdateUserTask.Params): Try<Unit> {
|
|
||||||
return monarchy.tryTransactionSync { realm ->
|
|
||||||
params.eventIds.forEach { eventId ->
|
|
||||||
val event = EventEntity.where(realm, eventId).findFirst()?.asDomain()
|
|
||||||
?: return@forEach
|
|
||||||
val roomId = event.roomId ?: return@forEach
|
|
||||||
val userId = event.stateKey ?: return@forEach
|
|
||||||
val roomMember = RoomMembers(realm, roomId).get(userId) ?: return@forEach
|
|
||||||
if (roomMember.membership != Membership.JOIN) return@forEach
|
|
||||||
|
|
||||||
val userEntity = UserEntity.where(realm, userId).findFirst()
|
|
||||||
?: realm.createObject(UserEntity::class.java, userId)
|
|
||||||
userEntity.displayName = roomMember.displayName ?: ""
|
|
||||||
userEntity.avatarUrl = roomMember.avatarUrl ?: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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.user
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||||
|
import im.vector.matrix.android.internal.database.model.UserEntity
|
||||||
|
|
||||||
|
internal object UserEntityFactory {
|
||||||
|
|
||||||
|
fun createOrNull(event: Event): UserEntity? {
|
||||||
|
if (event.type != EventType.STATE_ROOM_MEMBER) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val roomMember = event.content.toModel<RoomMember>() ?: return null
|
||||||
|
return UserEntity(event.stateKey ?: "",
|
||||||
|
roomMember.displayName ?: "",
|
||||||
|
roomMember.avatarUrl ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,60 +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.matrix.android.internal.session.user
|
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
|
||||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
|
||||||
import im.vector.matrix.android.internal.database.query.types
|
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
|
||||||
import im.vector.matrix.android.internal.task.TaskThread
|
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
|
||||||
import io.realm.OrderedCollectionChangeSet
|
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import io.realm.RealmResults
|
|
||||||
import io.realm.Sort
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
internal class UserEntityUpdater @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
|
|
||||||
private val updateUserTask: UpdateUserTask,
|
|
||||||
private val taskExecutor: TaskExecutor)
|
|
||||||
: RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
|
||||||
|
|
||||||
override val query = Monarchy.Query<EventEntity> {
|
|
||||||
EventEntity
|
|
||||||
.types(it, listOf(EventType.STATE_ROOM_MEMBER))
|
|
||||||
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
|
|
||||||
.distinct(EventEntityFields.STATE_KEY)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
|
||||||
val roomMembersEvents = changeSet.insertions
|
|
||||||
.asSequence()
|
|
||||||
.mapNotNull { results[it]?.eventId }
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
val taskParams = UpdateUserTask.Params(roomMembersEvents)
|
|
||||||
updateUserTask
|
|
||||||
.configureWith(taskParams)
|
|
||||||
.executeOn(TaskThread.IO)
|
|
||||||
.executeBy(taskExecutor)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -26,7 +26,4 @@ internal abstract class UserModule {
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindUserService(userService: DefaultUserService): UserService
|
abstract fun bindUserService(userService: DefaultUserService): UserService
|
||||||
|
|
||||||
@Binds
|
|
||||||
abstract fun bindUpdateUserTask(updateUserTask: DefaultUpdateUserTask): UpdateUserTask
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -119,7 +119,7 @@
|
||||||
<string name="verification_emoji_robot">Robota</string>
|
<string name="verification_emoji_robot">Robota</string>
|
||||||
<string name="verification_emoji_hat">Txanoa</string>
|
<string name="verification_emoji_hat">Txanoa</string>
|
||||||
<string name="verification_emoji_glasses">Betaurrekoak</string>
|
<string name="verification_emoji_glasses">Betaurrekoak</string>
|
||||||
<string name="verification_emoji_wrench">Giltza ingelesa</string>
|
<string name="verification_emoji_wrench">Giltza</string>
|
||||||
<string name="verification_emoji_santa">Santa</string>
|
<string name="verification_emoji_santa">Santa</string>
|
||||||
<string name="verification_emoji_thumbsup">Ederto</string>
|
<string name="verification_emoji_thumbsup">Ederto</string>
|
||||||
<string name="verification_emoji_umbrella">Aterkia</string>
|
<string name="verification_emoji_umbrella">Aterkia</string>
|
||||||
|
|
|
@ -2,4 +2,5 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="summary_message">%1$s: %2$s</string>
|
<string name="summary_message">%1$s: %2$s</string>
|
||||||
<string name="notice_room_invite_no_invitee">%s\'의 초대</string>
|
<string name="notice_room_invite_no_invitee">%s\'의 초대</string>
|
||||||
|
<string name="verification_emoji_headphone">헤드폰</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -228,4 +228,14 @@
|
||||||
<!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb -->
|
<!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb -->
|
||||||
<string name="verification_emoji_pin">Pin</string>
|
<string name="verification_emoji_pin">Pin</string>
|
||||||
|
|
||||||
|
<!-- Strings for RiotX -->
|
||||||
|
<string name="initial_sync_start_importing_account">Initial Sync:\nImporting account…</string>
|
||||||
|
<string name="initial_sync_start_importing_account_crypto">Initial Sync:\nImporting crypto</string>
|
||||||
|
<string name="initial_sync_start_importing_account_rooms">Initial Sync:\nImporting Rooms</string>
|
||||||
|
<string name="initial_sync_start_importing_account_joined_rooms">Initial Sync:\nImporting Joined Rooms</string>
|
||||||
|
<string name="initial_sync_start_importing_account_invited_rooms">Initial Sync:\nImporting Invited Rooms</string>
|
||||||
|
<string name="initial_sync_start_importing_account_left_rooms">Initial Sync:\nImporting Left Rooms</string>
|
||||||
|
<string name="initial_sync_start_importing_account_groups">Initial Sync:\nImporting Communities</string>
|
||||||
|
<string name="initial_sync_start_importing_account_data">Initial Sync:\nImporting Account Data</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="initial_sync_start_importing_account">Initial Sync:\nImporting account…</string>
|
|
||||||
<string name="initial_sync_start_importing_account_crypto">Initial Sync:\nImporting crypto</string>
|
|
||||||
<string name="initial_sync_start_importing_account_rooms">Initial Sync:\nImporting Rooms</string>
|
|
||||||
<string name="initial_sync_start_importing_account_joined_rooms">Initial Sync:\nImporting Joined Rooms</string>
|
|
||||||
<string name="initial_sync_start_importing_account_invited_rooms">Initial Sync:\nImporting Invited Rooms</string>
|
|
||||||
<string name="initial_sync_start_importing_account_left_rooms">Initial Sync:\nImporting Left Rooms</string>
|
|
||||||
<string name="initial_sync_start_importing_account_groups">Initial Sync:\nImporting Communities</string>
|
|
||||||
<string name="initial_sync_start_importing_account_data">Initial Sync:\nImporting Account Data</string>
|
|
||||||
</resources>
|
</resources>
|
|
@ -13,7 +13,7 @@ androidExtensions {
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.versionMajor = 0
|
ext.versionMajor = 0
|
||||||
ext.versionMinor = 1
|
ext.versionMinor = 2
|
||||||
ext.versionPatch = 0
|
ext.versionPatch = 0
|
||||||
|
|
||||||
static def getGitTimestamp() {
|
static def getGitTimestamp() {
|
||||||
|
@ -96,6 +96,9 @@ android {
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
resValue "string", "app_name", "RiotX dbg"
|
||||||
|
|
||||||
resValue "bool", "debug_mode", "true"
|
resValue "bool", "debug_mode", "true"
|
||||||
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
|
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
|
||||||
|
|
||||||
|
@ -103,6 +106,8 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
release {
|
release {
|
||||||
|
resValue "string", "app_name", "RiotX"
|
||||||
|
|
||||||
resValue "bool", "debug_mode", "false"
|
resValue "bool", "debug_mode", "false"
|
||||||
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
|
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
|
||||||
|
|
||||||
|
@ -182,8 +187,9 @@ dependencies {
|
||||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
||||||
implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.0'
|
implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.0'
|
||||||
// TODO RxBindings3 exists
|
// RXBinding
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.2.0'
|
implementation 'com.jakewharton.rxbinding3:rxbinding:3.0.0-alpha2'
|
||||||
|
implementation 'com.jakewharton.rxbinding3:rxbinding-appcompat:3.0.0-alpha2'
|
||||||
|
|
||||||
implementation("com.airbnb.android:epoxy:$epoxy_version")
|
implementation("com.airbnb.android:epoxy:$epoxy_version")
|
||||||
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
|
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
|
||||||
|
@ -249,6 +255,8 @@ dependencies {
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implementation 'diff_match_patch:diff_match_patch:current'
|
||||||
|
|
||||||
// TESTS
|
// TESTS
|
||||||
testImplementation 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||||
|
|
40
vector/src/gplay/debug/google-services.json
Normal file
40
vector/src/gplay/debug/google-services.json
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "912726360885",
|
||||||
|
"firebase_url": "https://vector-alpha.firebaseio.com",
|
||||||
|
"project_id": "vector-alpha",
|
||||||
|
"storage_bucket": "vector-alpha.appspot.com"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:912726360885:android:4ef8f3a0021e774d",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "im.vector.riotx.debug"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
|
@ -199,7 +199,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
if (eventType == null) {
|
if (eventType == null) {
|
||||||
//Just add a generic unknown event
|
//Just add a generic unknown event
|
||||||
val simpleNotifiableEvent = SimpleNotifiableEvent(
|
val simpleNotifiableEvent = SimpleNotifiableEvent(
|
||||||
session.sessionParams.credentials.userId,
|
session.myUserId,
|
||||||
eventId,
|
eventId,
|
||||||
true, //It's an issue in this case, all event will bing even if expected to be silent.
|
true, //It's an issue in this case, all event will bing even if expected to be silent.
|
||||||
title = getString(R.string.notification_unknown_new_event),
|
title = getString(R.string.notification_unknown_new_event),
|
||||||
|
@ -238,7 +238,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
notifiableEvent.isPushGatewayEvent = true
|
notifiableEvent.isPushGatewayEvent = true
|
||||||
notifiableEvent.matrixID = session.sessionParams.credentials.userId
|
notifiableEvent.matrixID = session.myUserId
|
||||||
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
|
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
|
||||||
notificationDrawerManager.refreshNotificationDrawer()
|
notificationDrawerManager.refreshNotificationDrawer()
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,8 +58,10 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".features.reactions.EmojiReactionPickerActivity"
|
android:name=".features.reactions.EmojiReactionPickerActivity"
|
||||||
android:label="@string/title_activity_emoji_reaction_picker" />
|
android:label="@string/title_activity_emoji_reaction_picker" />
|
||||||
|
<activity android:name=".features.roomdirectory.createroom.CreateRoomActivity" />
|
||||||
<activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
|
<activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
|
||||||
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
|
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
|
||||||
|
<activity android:name=".features.home.room.filtered.FilteredRoomsActivity" />
|
||||||
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
|
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
|
||||||
<activity android:name=".features.debug.DebugMenuActivity" />
|
<activity android:name=".features.debug.DebugMenuActivity" />
|
||||||
|
|
||||||
|
|
|
@ -344,6 +344,13 @@ SOFTWARE.
|
||||||
<br/>
|
<br/>
|
||||||
Copyright (c) 2018, Jaisel Rahman
|
Copyright (c) 2018, Jaisel Rahman
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>diff-match-patch</b>
|
||||||
|
<br/>
|
||||||
|
Copyright 2018 The diff-match-patch Authors. https://github.com/google/diff-match-patch
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<pre>
|
<pre>
|
||||||
Apache License
|
Apache License
|
||||||
|
|
|
@ -32,13 +32,14 @@ import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupStep1Frag
|
||||||
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupStep2Fragment
|
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupStep2Fragment
|
||||||
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupStep3Fragment
|
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupStep3Fragment
|
||||||
import im.vector.riotx.features.crypto.verification.SASVerificationIncomingFragment
|
import im.vector.riotx.features.crypto.verification.SASVerificationIncomingFragment
|
||||||
import im.vector.riotx.features.home.*
|
import im.vector.riotx.features.home.HomeActivity
|
||||||
|
import im.vector.riotx.features.home.HomeDetailFragment
|
||||||
|
import im.vector.riotx.features.home.HomeDrawerFragment
|
||||||
|
import im.vector.riotx.features.home.HomeModule
|
||||||
import im.vector.riotx.features.home.group.GroupListFragment
|
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.timeline.action.MessageActionsBottomSheet
|
import im.vector.riotx.features.home.room.detail.timeline.action.*
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment
|
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionFragment
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
|
|
||||||
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
|
||||||
import im.vector.riotx.features.login.LoginActivity
|
import im.vector.riotx.features.login.LoginActivity
|
||||||
|
@ -50,6 +51,7 @@ import im.vector.riotx.features.rageshake.RageShake
|
||||||
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
|
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
|
||||||
import im.vector.riotx.features.roomdirectory.PublicRoomsFragment
|
import im.vector.riotx.features.roomdirectory.PublicRoomsFragment
|
||||||
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
|
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
|
||||||
|
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
|
||||||
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
|
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
|
||||||
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
|
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
|
||||||
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
|
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
|
||||||
|
@ -93,6 +95,8 @@ interface ScreenComponent {
|
||||||
|
|
||||||
fun inject(viewReactionBottomSheet: ViewReactionBottomSheet)
|
fun inject(viewReactionBottomSheet: ViewReactionBottomSheet)
|
||||||
|
|
||||||
|
fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
|
||||||
|
|
||||||
fun inject(messageMenuFragment: MessageMenuFragment)
|
fun inject(messageMenuFragment: MessageMenuFragment)
|
||||||
|
|
||||||
fun inject(vectorSettingsActivity: VectorSettingsActivity)
|
fun inject(vectorSettingsActivity: VectorSettingsActivity)
|
||||||
|
@ -131,6 +135,10 @@ interface ScreenComponent {
|
||||||
|
|
||||||
fun inject(imageMediaViewerActivity: ImageMediaViewerActivity)
|
fun inject(imageMediaViewerActivity: ImageMediaViewerActivity)
|
||||||
|
|
||||||
|
fun inject(filteredRoomsActivity: FilteredRoomsActivity)
|
||||||
|
|
||||||
|
fun inject(createRoomActivity: CreateRoomActivity)
|
||||||
|
|
||||||
fun inject(vectorInviteView: VectorInviteView)
|
fun inject(vectorInviteView: VectorInviteView)
|
||||||
|
|
||||||
fun inject(videoMediaViewerActivity: VideoMediaViewerActivity)
|
fun inject(videoMediaViewerActivity: VideoMediaViewerActivity)
|
||||||
|
|
|
@ -29,11 +29,7 @@ import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsVie
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel_AssistedFactory
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel_AssistedFactory
|
||||||
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel
|
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupSharedViewModel
|
||||||
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
|
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
|
||||||
import im.vector.riotx.features.home.HomeActivityViewModel
|
import im.vector.riotx.features.home.*
|
||||||
import im.vector.riotx.features.home.HomeActivityViewModel_AssistedFactory
|
|
||||||
import im.vector.riotx.features.home.HomeDetailViewModel
|
|
||||||
import im.vector.riotx.features.home.HomeDetailViewModel_AssistedFactory
|
|
||||||
import im.vector.riotx.features.home.HomeNavigationViewModel
|
|
||||||
import im.vector.riotx.features.home.group.GroupListViewModel
|
import im.vector.riotx.features.home.group.GroupListViewModel
|
||||||
import im.vector.riotx.features.home.group.GroupListViewModel_AssistedFactory
|
import im.vector.riotx.features.home.group.GroupListViewModel_AssistedFactory
|
||||||
import im.vector.riotx.features.home.room.detail.RoomDetailViewModel
|
import im.vector.riotx.features.home.room.detail.RoomDetailViewModel
|
||||||
|
@ -59,11 +55,17 @@ import im.vector.riotx.features.workers.signout.SignOutViewModel
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
interface ViewModelModule {
|
interface ViewModelModule {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModels with @IntoMap will be injected by this factory
|
||||||
|
*/
|
||||||
@Binds
|
@Binds
|
||||||
fun bindViewModelFactory(factory: VectorViewModelFactory): ViewModelProvider.Factory
|
fun bindViewModelFactory(factory: VectorViewModelFactory): ViewModelProvider.Factory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Below are bindings for the androidx view models (which extend ViewModel). Will be converted to MvRx ViewModel in the future.
|
||||||
|
*/
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ViewModelKey(SignOutViewModel::class)
|
@ViewModelKey(SignOutViewModel::class)
|
||||||
|
@ -114,6 +116,10 @@ interface ViewModelModule {
|
||||||
@ViewModelKey(ConfigurationViewModel::class)
|
@ViewModelKey(ConfigurationViewModel::class)
|
||||||
fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel
|
fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Below are bindings for the MvRx view models (which extend VectorViewModel). Will be the only usage in the future.
|
||||||
|
*/
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindHomeActivityViewModelFactory(factory: HomeActivityViewModel_AssistedFactory): HomeActivityViewModel.Factory
|
fun bindHomeActivityViewModelFactory(factory: HomeActivityViewModel_AssistedFactory): HomeActivityViewModel.Factory
|
||||||
|
|
||||||
|
@ -156,6 +162,9 @@ interface ViewModelModule {
|
||||||
@Binds
|
@Binds
|
||||||
fun bindViewReactionViewModelFactory(factory: ViewReactionViewModel_AssistedFactory): ViewReactionViewModel.Factory
|
fun bindViewReactionViewModelFactory(factory: ViewReactionViewModel_AssistedFactory): ViewReactionViewModel.Factory
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindViewEditHistoryViewModelFactory(factory: ViewEditHistoryViewModel_AssistedFactory): ViewEditHistoryViewModel.Factory
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
fun bindCreateRoomViewModelFactory(factory: CreateRoomViewModel_AssistedFactory): CreateRoomViewModel.Factory
|
fun bindCreateRoomViewModelFactory(factory: CreateRoomViewModel_AssistedFactory): CreateRoomViewModel.Factory
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,8 @@ fun AppCompatActivity.addFragment(fragment: Fragment, frameId: Int) {
|
||||||
supportFragmentManager.inTransaction { add(frameId, fragment) }
|
supportFragmentManager.inTransaction { add(frameId, fragment) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int) {
|
fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int, tag: String? = null) {
|
||||||
supportFragmentManager.inTransaction { replace(frameId, fragment) }
|
supportFragmentManager.inTransaction { replace(frameId, fragment, tag) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun AppCompatActivity.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
fun AppCompatActivity.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
|
||||||
|
|
|
@ -16,14 +16,20 @@
|
||||||
|
|
||||||
package im.vector.riotx.core.extensions
|
package im.vector.riotx.core.extensions
|
||||||
|
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.sync.FilterService
|
import im.vector.matrix.android.api.session.sync.FilterService
|
||||||
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener) {
|
fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener) {
|
||||||
open()
|
open()
|
||||||
setFilter(FilterService.FilterPreset.RiotFilter)
|
setFilter(FilterService.FilterPreset.RiotFilter)
|
||||||
startSync()
|
Timber.i("Configure and start session for ${this.myUserId}")
|
||||||
|
val isAtLeastStarted = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
|
||||||
|
Timber.v("--> is at least started? $isAtLeastStarted")
|
||||||
|
startSync(isAtLeastStarted)
|
||||||
refreshPushers()
|
refreshPushers()
|
||||||
pushRuleTriggerListener.startWithSession(this)
|
pushRuleTriggerListener.startWithSession(this)
|
||||||
fetchPushRules()
|
fetchPushRules()
|
||||||
|
|
|
@ -21,5 +21,5 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
|
||||||
fun TimelineEvent.canReact(): Boolean {
|
fun TimelineEvent.canReact(): Boolean {
|
||||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||||
return root.getClearType() == EventType.MESSAGE && sendState.isSent()
|
return root.getClearType() == EventType.MESSAGE && sendState.isSent() && !root.isRedacted()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,18 @@ package im.vector.riotx.core.platform
|
||||||
|
|
||||||
import com.airbnb.mvrx.BaseMvRxViewModel
|
import com.airbnb.mvrx.BaseMvRxViewModel
|
||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import im.vector.matrix.android.api.util.CancelableBag
|
||||||
import im.vector.riotx.BuildConfig
|
import im.vector.riotx.BuildConfig
|
||||||
|
|
||||||
abstract class VectorViewModel<S : MvRxState>(initialState: S)
|
abstract class VectorViewModel<S : MvRxState>(initialState: S)
|
||||||
: BaseMvRxViewModel<S>(initialState, false)
|
: BaseMvRxViewModel<S>(initialState, false) {
|
||||||
|
|
||||||
|
protected val cancelableBag = CancelableBag()
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
cancelableBag.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -58,10 +58,10 @@ open class UserAvatarPreference : Preference {
|
||||||
open fun refreshAvatar() {
|
open fun refreshAvatar() {
|
||||||
val session = mSession ?: return
|
val session = mSession ?: return
|
||||||
val view = mAvatarView ?: return
|
val view = mAvatarView ?: return
|
||||||
session.getUser(session.sessionParams.credentials.userId)?.let {
|
session.getUser(session.myUserId)?.let {
|
||||||
avatarRenderer.render(it, view)
|
avatarRenderer.render(it, view)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
avatarRenderer.render(null, session.sessionParams.credentials.userId, null, view)
|
avatarRenderer.render(null, session.myUserId, null, view)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
|
||||||
var title: String? = null
|
var title: String? = null
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var description: String? = null
|
var description: CharSequence? = null
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var style: STYLE = STYLE.NORMAL_TEXT
|
var style: STYLE = STYLE.NORMAL_TEXT
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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.ui.list
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic list item header left aligned with notice color.
|
||||||
|
*/
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_generic_header)
|
||||||
|
abstract class GenericItemHeader : VectorEpoxyModel<GenericItemHeader.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var text: String? = null
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
holder.text.setTextOrHide(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder() {
|
||||||
|
val text by bind<TextView>(R.id.itemGenericHeaderText)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package im.vector.riotx.core.ui.list
|
||||||
|
|
||||||
|
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 generic list item header left aligned with notice color.
|
||||||
|
*/
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_generic_loader)
|
||||||
|
abstract class GenericLoaderItem : VectorEpoxyModel<GenericLoaderItem.Holder>() {
|
||||||
|
|
||||||
|
//Maybe/Later add some style configuration, SMALL/BIG ?
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {}
|
||||||
|
|
||||||
|
class Holder : VectorEpoxyHolder()
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
@ -81,11 +82,11 @@ fun requestDisablingBatteryOptimization(activity: Activity, fragment: Fragment?,
|
||||||
* @param context the context
|
* @param context the context
|
||||||
* @param text the text to copy
|
* @param text the text to copy
|
||||||
*/
|
*/
|
||||||
fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = true) {
|
fun copyToClipboard(context: Context, text: CharSequence, showToast: Boolean = true, @StringRes toastMessage : Int = R.string.copied_to_clipboard) {
|
||||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
clipboard.primaryClip = ClipData.newPlainText("", text)
|
clipboard.primaryClip = ClipData.newPlainText("", text)
|
||||||
if (showToast) {
|
if (showToast) {
|
||||||
context.toast(R.string.copied_to_clipboard)
|
context.toast(toastMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,8 @@ import androidx.lifecycle.ViewModel
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.listeners.StepProgressListener
|
import im.vector.matrix.android.api.listeners.StepProgressListener
|
||||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
||||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
||||||
|
@ -57,7 +57,7 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor() : ViewModel() {
|
||||||
keysBackup.restoreKeysWithRecoveryKey(keysVersionResult,
|
keysBackup.restoreKeysWithRecoveryKey(keysVersionResult,
|
||||||
recoveryKey,
|
recoveryKey,
|
||||||
null,
|
null,
|
||||||
session.sessionParams.credentials.userId,
|
session.myUserId,
|
||||||
object : StepProgressListener {
|
object : StepProgressListener {
|
||||||
override fun onStepProgress(step: StepProgressListener.Step) {
|
override fun onStepProgress(step: StepProgressListener.Step) {
|
||||||
when (step) {
|
when (step) {
|
||||||
|
|
|
@ -21,8 +21,8 @@ import androidx.lifecycle.ViewModel
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.listeners.StepProgressListener
|
import im.vector.matrix.android.api.listeners.StepProgressListener
|
||||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
||||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
||||||
|
@ -58,7 +58,7 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor() : ViewModel
|
||||||
keysBackup.restoreKeyBackupWithPassword(keysVersionResult,
|
keysBackup.restoreKeyBackupWithPassword(keysVersionResult,
|
||||||
passphrase.value!!,
|
passphrase.value!!,
|
||||||
null,
|
null,
|
||||||
sharedViewModel.session.sessionParams.credentials.userId,
|
sharedViewModel.session.myUserId,
|
||||||
object : StepProgressListener {
|
object : StepProgressListener {
|
||||||
override fun onStepProgress(step: StepProgressListener.Step) {
|
override fun onStepProgress(step: StepProgressListener.Step) {
|
||||||
when (step) {
|
when (step) {
|
||||||
|
|
|
@ -41,6 +41,7 @@ import im.vector.riotx.core.platform.ToolbarConfigurable
|
||||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
import im.vector.riotx.core.pushers.PushersManager
|
import im.vector.riotx.core.pushers.PushersManager
|
||||||
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
|
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
|
||||||
|
import im.vector.riotx.features.navigation.Navigator
|
||||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||||
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
|
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
|
||||||
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
||||||
|
@ -64,6 +65,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||||
@Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory
|
@Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory
|
||||||
@Inject lateinit var homeNavigator: HomeNavigator
|
@Inject lateinit var homeNavigator: HomeNavigator
|
||||||
|
@Inject lateinit var navigator: Navigator
|
||||||
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
|
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
|
||||||
@Inject lateinit var pushManager: PushersManager
|
@Inject lateinit var pushManager: PushersManager
|
||||||
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
||||||
|
@ -192,6 +194,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
bugReporter.openBugReportScreen(this, false)
|
bugReporter.openBugReportScreen(this, false)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
R.id.menu_home_filter -> {
|
||||||
|
navigator.openRoomsFiltering(this)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -209,7 +209,7 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
|
||||||
unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
|
unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
|
||||||
unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
|
unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
|
||||||
syncProgressBar.visibility = when (it.syncState) {
|
syncProgressBar.visibility = when (it.syncState) {
|
||||||
is SyncState.RUNNING -> if (it.syncState.catchingUp) View.VISIBLE else View.GONE
|
is SyncState.RUNNING -> if (it.syncState.afterPause) View.VISIBLE else View.GONE
|
||||||
else -> View.GONE
|
else -> View.GONE
|
||||||
}
|
}
|
||||||
syncProgressBarWrap.visibility = syncProgressBar.visibility
|
syncProgressBarWrap.visibility = syncProgressBar.visibility
|
||||||
|
|
|
@ -52,7 +52,7 @@ class HomeDrawerFragment : VectorBaseFragment() {
|
||||||
replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer)
|
replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
session.observeUser(session.sessionParams.credentials.userId).observeK(this) { user ->
|
session.observeUser(session.myUserId).observeK(this) { user ->
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView)
|
avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView)
|
||||||
homeDrawerUsernameView.text = user.displayName
|
homeDrawerUsernameView.text = user.displayName
|
||||||
|
|
|
@ -18,10 +18,6 @@ package im.vector.riotx.features.home
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.riotx.core.utils.RxStore
|
import im.vector.riotx.core.utils.RxStore
|
||||||
import im.vector.riotx.features.home.room.list.RoomListDisplayModeFilter
|
|
||||||
import im.vector.riotx.features.home.room.list.RoomListFragment
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
|
||||||
.rx()
|
.rx()
|
||||||
.liveGroupSummaries()
|
.liveGroupSummaries()
|
||||||
.map {
|
.map {
|
||||||
val myUser = session.getUser(session.sessionParams.credentials.userId)
|
val myUser = session.getUser(session.myUserId)
|
||||||
val allCommunityGroup = GroupSummary(
|
val allCommunityGroup = GroupSummary(
|
||||||
groupId = ALL_COMMUNITIES_GROUP_ID,
|
groupId = ALL_COMMUNITIES_GROUP_ID,
|
||||||
displayName = stringProvider.getString(R.string.group_all_communities),
|
displayName = stringProvider.getString(R.string.group_all_communities),
|
||||||
|
|
|
@ -32,7 +32,6 @@ sealed class 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 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 ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
|
|
||||||
data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
|
data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
|
||||||
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions()
|
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions()
|
||||||
object AcceptInvite : RoomDetailActions()
|
object AcceptInvite : RoomDetailActions()
|
||||||
|
|
|
@ -35,7 +35,7 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if (isFirstCreation()) {
|
if (isFirstCreation()) {
|
||||||
val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS)
|
val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS)
|
||||||
?: return
|
?: return
|
||||||
val roomDetailFragment = RoomDetailFragment.newInstance(roomDetailArgs)
|
val roomDetailFragment = RoomDetailFragment.newInstance(roomDetailArgs)
|
||||||
replaceFragment(roomDetailFragment, R.id.roomDetailContainer)
|
replaceFragment(roomDetailFragment, R.id.roomDetailContainer)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,9 +37,11 @@ import androidx.annotation.DrawableRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import butterknife.BindView
|
import butterknife.BindView
|
||||||
|
import com.airbnb.epoxy.EpoxyModel
|
||||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.args
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
@ -57,8 +59,10 @@ import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.Membership
|
import im.vector.matrix.android.api.session.room.model.Membership
|
||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
|
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.getTextEditableContent
|
||||||
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
|
||||||
|
@ -85,12 +89,9 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerView
|
||||||
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel
|
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.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler
|
import im.vector.riotx.features.home.room.detail.timeline.action.*
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuViewModel
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.riotx.features.home.room.detail.timeline.item.*
|
||||||
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
|
||||||
|
@ -261,7 +262,7 @@ class RoomDetailFragment :
|
||||||
composerLayout.composerRelatedMessageContent.text = formattedBody
|
composerLayout.composerRelatedMessageContent.text = formattedBody
|
||||||
?: nonFormattedBody
|
?: nonFormattedBody
|
||||||
|
|
||||||
composerLayout.composerEditText.setText(if (useText) nonFormattedBody else "")
|
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
|
||||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
||||||
|
|
||||||
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
||||||
|
@ -269,8 +270,10 @@ class RoomDetailFragment :
|
||||||
|
|
||||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||||
composerLayout.expand {
|
composerLayout.expand {
|
||||||
|
//need to do it here also when not using quick reply
|
||||||
focusComposerAndShowKeyboard()
|
focusComposerAndShowKeyboard()
|
||||||
}
|
}
|
||||||
|
focusComposerAndShowKeyboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -326,6 +329,32 @@ class RoomDetailFragment :
|
||||||
})
|
})
|
||||||
recyclerView.setController(timelineEventController)
|
recyclerView.setController(timelineEventController)
|
||||||
timelineEventController.callback = this
|
timelineEventController.callback = this
|
||||||
|
|
||||||
|
if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) {
|
||||||
|
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
|
||||||
|
R.drawable.ic_reply,
|
||||||
|
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
||||||
|
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
||||||
|
(model as? AbsMessageItem)?.informationData?.let {
|
||||||
|
val eventId = it.eventId
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
||||||
|
return when (model) {
|
||||||
|
is MessageFileItem,
|
||||||
|
is MessageImageVideoItem,
|
||||||
|
is MessageTextItem -> {
|
||||||
|
return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
val touchHelper = ItemTouchHelper(swipeCallback)
|
||||||
|
touchHelper.attachToRecyclerView(recyclerView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupComposer() {
|
private fun setupComposer() {
|
||||||
|
@ -489,7 +518,7 @@ class RoomDetailFragment :
|
||||||
timelineEventController.setTimeline(state.timeline, state.eventId)
|
timelineEventController.setTimeline(state.timeline, state.eventId)
|
||||||
inviteView.visibility = View.GONE
|
inviteView.visibility = View.GONE
|
||||||
|
|
||||||
val uid = session.sessionParams.credentials.userId
|
val uid = session.myUserId
|
||||||
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
|
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
|
||||||
avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)
|
avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)
|
||||||
|
|
||||||
|
@ -575,7 +604,7 @@ class RoomDetailFragment :
|
||||||
|
|
||||||
override fun onUrlLongClicked(url: String): Boolean {
|
override fun onUrlLongClicked(url: String): Boolean {
|
||||||
// Copy the url to the clipboard
|
// Copy the url to the clipboard
|
||||||
copyToClipboard(requireContext(), url)
|
copyToClipboard(requireContext(), url, true, R.string.link_copied_to_clipboard)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -666,10 +695,8 @@ class RoomDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) {
|
override fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) {
|
||||||
editAggregatedSummary?.also {
|
ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
|
||||||
roomDetailViewModel.process(RoomDetailActions.ShowEditHistoryAction(informationData.eventId, it))
|
.show(requireActivity().supportFragmentManager, "DISPLAY_EDITS")
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
// AutocompleteUserPresenter.Callback
|
// AutocompleteUserPresenter.Callback
|
||||||
|
|
||||||
|
@ -785,7 +812,7 @@ class RoomDetailFragment :
|
||||||
if (null != text) {
|
if (null != text) {
|
||||||
// var vibrate = false
|
// var vibrate = false
|
||||||
|
|
||||||
val myDisplayName = session.getUser(session.sessionParams.credentials.userId)?.displayName
|
val myDisplayName = session.getUser(session.myUserId)?.displayName
|
||||||
if (TextUtils.equals(myDisplayName, text)) {
|
if (TextUtils.equals(myDisplayName, text)) {
|
||||||
// current user
|
// current user
|
||||||
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
|
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
|
||||||
|
@ -833,10 +860,12 @@ class RoomDetailFragment :
|
||||||
// VectorInviteView.Callback
|
// VectorInviteView.Callback
|
||||||
|
|
||||||
override fun onAcceptInvite() {
|
override fun onAcceptInvite() {
|
||||||
|
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
|
||||||
roomDetailViewModel.process(RoomDetailActions.AcceptInvite)
|
roomDetailViewModel.process(RoomDetailActions.AcceptInvite)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRejectInvite() {
|
override fun onRejectInvite() {
|
||||||
|
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
|
||||||
roomDetailViewModel.process(RoomDetailActions.RejectInvite)
|
roomDetailViewModel.process(RoomDetailActions.RejectInvite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,8 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
|
||||||
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.internal.crypto.attachments.toElementToDecrypt
|
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotx.R
|
|
||||||
import im.vector.riotx.core.intent.getFilenameFromUri
|
import im.vector.riotx.core.intent.getFilenameFromUri
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.core.resources.UserPreferencesProvider
|
import im.vector.riotx.core.resources.UserPreferencesProvider
|
||||||
|
@ -52,8 +52,6 @@ import org.commonmark.parser.Parser
|
||||||
import org.commonmark.renderer.html.HtmlRenderer
|
import org.commonmark.renderer.html.HtmlRenderer
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
|
||||||
|
@ -97,7 +95,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
observeRoomSummary()
|
observeRoomSummary()
|
||||||
observeEventDisplayedActions()
|
observeEventDisplayedActions()
|
||||||
observeInvitationState()
|
observeInvitationState()
|
||||||
room.loadRoomMembersIfNeeded()
|
cancelableBag += room.loadRoomMembersIfNeeded()
|
||||||
timeline.start()
|
timeline.start()
|
||||||
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
||||||
}
|
}
|
||||||
|
@ -114,7 +112,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
|
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
|
||||||
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
|
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
|
||||||
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
|
||||||
is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action)
|
|
||||||
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
|
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
|
||||||
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
|
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
|
||||||
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
|
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
|
||||||
|
@ -230,16 +227,27 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is SendMode.EDIT -> {
|
is SendMode.EDIT -> {
|
||||||
val messageContent: MessageContent? =
|
|
||||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
|
||||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
|
||||||
val nonFormattedBody = messageContent?.body ?: ""
|
|
||||||
|
|
||||||
if (nonFormattedBody != action.text) {
|
//is original event a reply?
|
||||||
room.editTextMessage(state.sendMode.timelineEvent.root.eventId
|
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
|
||||||
?: "", messageContent?.type ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
|
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
|
||||||
|
if (inReplyTo != null) {
|
||||||
|
//TODO check if same content?
|
||||||
|
room.getTimeLineEvent(inReplyTo)?.let {
|
||||||
|
room.editReply(state.sendMode.timelineEvent, it, action.text)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Same message content, do not send edition")
|
val messageContent: MessageContent? =
|
||||||
|
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||||
|
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||||
|
val existingBody = messageContent?.body ?: ""
|
||||||
|
if (existingBody != action.text) {
|
||||||
|
room.editTextMessage(state.sendMode.timelineEvent.root.eventId
|
||||||
|
?: "", messageContent?.type
|
||||||
|
?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
|
||||||
|
} else {
|
||||||
|
Timber.w("Same message content, do not send edition")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
|
@ -309,22 +317,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
return finalText
|
return finalText
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShowEditHistoryReaction(action: RoomDetailActions.ShowEditHistoryAction) {
|
|
||||||
//TODO temporary implementation
|
|
||||||
val lastReplace = action.editAggregatedSummary.sourceEvents.lastOrNull()?.let {
|
|
||||||
room.getTimeLineEvent(it)
|
|
||||||
} ?: return
|
|
||||||
|
|
||||||
val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
|
|
||||||
_nonBlockingPopAlert.postValue(LiveEvent(
|
|
||||||
Pair(R.string.last_edited_info_message, listOf(
|
|
||||||
lastReplace.getDisambiguatedDisplayName(),
|
|
||||||
dateFormat.format(Date(lastReplace.root.originServerTs ?: 0)))
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
|
private fun handleChangeTopicSlashCommand(changeTopic: ParsedCommand.ChangeTopic) {
|
||||||
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
|
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
|
||||||
|
|
||||||
|
@ -364,7 +356,7 @@ 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.sessionParams.credentials.userId)
|
room.undoReaction(action.key, action.targetEventId, session.myUserId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -372,7 +364,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
if (action.add) {
|
if (action.add) {
|
||||||
room.sendReaction(action.selectedReaction, action.targetEventId)
|
room.sendReaction(action.selectedReaction, action.targetEventId)
|
||||||
} else {
|
} else {
|
||||||
room.undoReaction(action.selectedReaction, action.targetEventId, session.sessionParams.credentials.userId)
|
room.undoReaction(action.selectedReaction, action.targetEventId, session.myUserId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,214 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package im.vector.riotx.features.home.room.detail
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_SWIPE
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.airbnb.epoxy.EpoxyModel
|
||||||
|
import com.airbnb.epoxy.EpoxyTouchHelperCallback
|
||||||
|
import com.airbnb.epoxy.EpoxyViewHolder
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
|
class RoomMessageTouchHelperCallback(private val context: Context,
|
||||||
|
@DrawableRes actionIcon: Int,
|
||||||
|
private val handler: QuickReplayHandler) : EpoxyTouchHelperCallback() {
|
||||||
|
|
||||||
|
interface QuickReplayHandler {
|
||||||
|
fun performQuickReplyOnHolder(model: EpoxyModel<*>)
|
||||||
|
fun canSwipeModel(model: EpoxyModel<*>): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
private var swipeBack: Boolean = false
|
||||||
|
private var dX = 0f
|
||||||
|
private var startTracking = false
|
||||||
|
private var isVibrate = false
|
||||||
|
|
||||||
|
private var replyButtonProgress: Float = 0F
|
||||||
|
private var lastReplyButtonAnimationTime: Long = 0
|
||||||
|
|
||||||
|
private var imageDrawable: Drawable = ContextCompat.getDrawable(context, actionIcon)!!
|
||||||
|
|
||||||
|
|
||||||
|
private val triggerDistance = convertToPx(100)
|
||||||
|
private val minShowDistance = convertToPx(20)
|
||||||
|
private val triggerDelta = convertToPx(20)
|
||||||
|
|
||||||
|
override fun onSwiped(viewHolder: EpoxyViewHolder?, direction: Int) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMove(recyclerView: RecyclerView?, viewHolder: EpoxyViewHolder?, target: EpoxyViewHolder?): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: EpoxyViewHolder): Int {
|
||||||
|
if (handler.canSwipeModel(viewHolder.model)) {
|
||||||
|
return ItemTouchHelper.Callback.makeMovementFlags(0, ItemTouchHelper.START) //Should we use Left?
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//We never let items completely go out
|
||||||
|
override fun convertToAbsoluteDirection(flags: Int, layoutDirection: Int): Int {
|
||||||
|
if (swipeBack) {
|
||||||
|
swipeBack = false
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return super.convertToAbsoluteDirection(flags, layoutDirection)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChildDraw(c: Canvas,
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: EpoxyViewHolder,
|
||||||
|
dX: Float,
|
||||||
|
dY: Float,
|
||||||
|
actionState: Int,
|
||||||
|
isCurrentlyActive: Boolean) {
|
||||||
|
if (actionState == ACTION_STATE_SWIPE) {
|
||||||
|
setTouchListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||||
|
}
|
||||||
|
val size = triggerDistance
|
||||||
|
if (Math.abs(viewHolder.itemView.translationX) < size || dX > this.dX /*going back*/) {
|
||||||
|
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||||
|
this.dX = dX
|
||||||
|
startTracking = true
|
||||||
|
}
|
||||||
|
drawReplyButton(c, viewHolder.itemView)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
private fun setTouchListener(c: Canvas,
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: EpoxyViewHolder,
|
||||||
|
dX: Float,
|
||||||
|
dY: Float,
|
||||||
|
actionState: Int,
|
||||||
|
isCurrentlyActive: Boolean) {
|
||||||
|
//TODO can this interfer with other interactions? should i remove it
|
||||||
|
recyclerView.setOnTouchListener { v, event ->
|
||||||
|
swipeBack = event.action == MotionEvent.ACTION_CANCEL || event.action == MotionEvent.ACTION_UP
|
||||||
|
if (swipeBack) {
|
||||||
|
if (Math.abs(dX) >= triggerDistance) {
|
||||||
|
try {
|
||||||
|
viewHolder.model?.let { handler.performQuickReplyOnHolder(it) }
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun drawReplyButton(canvas: Canvas, itemView: View) {
|
||||||
|
|
||||||
|
Timber.v("drawReplyButton")
|
||||||
|
val translationX = Math.abs(itemView.translationX)
|
||||||
|
val newTime = System.currentTimeMillis()
|
||||||
|
val dt = Math.min(17, newTime - lastReplyButtonAnimationTime)
|
||||||
|
lastReplyButtonAnimationTime = newTime
|
||||||
|
val showing = translationX >= minShowDistance
|
||||||
|
if (showing) {
|
||||||
|
if (replyButtonProgress < 1.0f) {
|
||||||
|
replyButtonProgress += dt / 180.0f
|
||||||
|
if (replyButtonProgress > 1.0f) {
|
||||||
|
replyButtonProgress = 1.0f
|
||||||
|
} else {
|
||||||
|
itemView.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (translationX <= 0.0f) {
|
||||||
|
replyButtonProgress = 0f
|
||||||
|
startTracking = false
|
||||||
|
isVibrate = false
|
||||||
|
} else {
|
||||||
|
if (replyButtonProgress > 0.0f) {
|
||||||
|
replyButtonProgress -= dt / 180.0f
|
||||||
|
if (replyButtonProgress < 0.1f) {
|
||||||
|
replyButtonProgress = 0f
|
||||||
|
} else {
|
||||||
|
itemView.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val alpha: Int
|
||||||
|
val scale: Float
|
||||||
|
if (showing) {
|
||||||
|
scale = if (replyButtonProgress <= 0.8f) {
|
||||||
|
1.2f * (replyButtonProgress / 0.8f)
|
||||||
|
} else {
|
||||||
|
1.2f - 0.2f * ((replyButtonProgress - 0.8f) / 0.2f)
|
||||||
|
}
|
||||||
|
alpha = Math.min(255f, 255 * (replyButtonProgress / 0.8f)).toInt()
|
||||||
|
} else {
|
||||||
|
scale = replyButtonProgress
|
||||||
|
alpha = Math.min(255f, 255 * replyButtonProgress).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
imageDrawable.alpha = alpha
|
||||||
|
if (startTracking) {
|
||||||
|
if (!isVibrate && translationX >= triggerDistance) {
|
||||||
|
itemView.performHapticFeedback(
|
||||||
|
HapticFeedbackConstants.LONG_PRESS
|
||||||
|
// , HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
|
||||||
|
)
|
||||||
|
isVibrate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val x: Int = itemView.width - if (translationX > triggerDistance + triggerDelta) {
|
||||||
|
(convertToPx(130) / 2).toInt()
|
||||||
|
} else {
|
||||||
|
(translationX / 2).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val y = (itemView.top + itemView.measuredHeight / 2).toFloat()
|
||||||
|
//magic numbers?
|
||||||
|
imageDrawable.setBounds(
|
||||||
|
(x - convertToPx(12) * scale).toInt(),
|
||||||
|
(y - convertToPx(11) * scale).toInt(),
|
||||||
|
(x + convertToPx(12) * scale).toInt(),
|
||||||
|
(y + convertToPx(10) * scale).toInt()
|
||||||
|
)
|
||||||
|
imageDrawable.draw(canvas)
|
||||||
|
imageDrawable.alpha = 255
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun convertToPx(dp: Int): Float {
|
||||||
|
return TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
dp.toFloat(),
|
||||||
|
context.resources.displayMetrics
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -57,6 +57,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||||
|
|
||||||
var currentConstraintSetId: Int = -1
|
var currentConstraintSetId: Int = -1
|
||||||
|
|
||||||
|
private val animationDuration = 100L
|
||||||
|
|
||||||
init {
|
init {
|
||||||
inflate(context, R.layout.merge_composer_layout, this)
|
inflate(context, R.layout.merge_composer_layout, this)
|
||||||
|
@ -73,7 +74,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||||
currentConstraintSetId = R.layout.constraint_set_composer_layout_compact
|
currentConstraintSetId = R.layout.constraint_set_composer_layout_compact
|
||||||
if (animate) {
|
if (animate) {
|
||||||
val transition = AutoTransition()
|
val transition = AutoTransition()
|
||||||
// transition.duration = 5000
|
transition.duration = animationDuration
|
||||||
transition.addListener(object : Transition.TransitionListener {
|
transition.addListener(object : Transition.TransitionListener {
|
||||||
|
|
||||||
override fun onTransitionEnd(transition: Transition) {
|
override fun onTransitionEnd(transition: Transition) {
|
||||||
|
@ -105,7 +106,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||||
currentConstraintSetId = R.layout.constraint_set_composer_layout_expanded
|
currentConstraintSetId = R.layout.constraint_set_composer_layout_expanded
|
||||||
if (animate) {
|
if (animate) {
|
||||||
val transition = AutoTransition()
|
val transition = AutoTransition()
|
||||||
// transition.duration = 5000
|
transition.duration = animationDuration
|
||||||
transition.addListener(object : Transition.TransitionListener {
|
transition.addListener(object : Transition.TransitionListener {
|
||||||
|
|
||||||
override fun onTransitionEnd(transition: Transition) {
|
override fun onTransitionEnd(transition: Transition) {
|
||||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.riotx.features.home.room.detail.timeline
|
||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.LongSparseArray
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
|
@ -84,7 +83,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||||
}
|
}
|
||||||
|
|
||||||
private val collapsedEventIds = linkedSetOf<Long>()
|
private val collapsedEventIds = linkedSetOf<Long>()
|
||||||
private val mergeItemCollapseStates = HashMap<Long,Boolean>()
|
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
|
||||||
private val modelCache = arrayListOf<CacheItemData?>()
|
private val modelCache = arrayListOf<CacheItemData?>()
|
||||||
|
|
||||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||||
|
@ -178,16 +177,19 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun buildModels() {
|
override fun buildModels() {
|
||||||
LoadingItem_()
|
val loaderAdded = LoadingItem_()
|
||||||
.id("forward_loading_item")
|
.id("forward_loading_item")
|
||||||
.addWhen(Timeline.Direction.FORWARDS)
|
.addWhen(Timeline.Direction.FORWARDS)
|
||||||
|
|
||||||
val timelineModels = getModels()
|
val timelineModels = getModels()
|
||||||
add(timelineModels)
|
add(timelineModels)
|
||||||
|
|
||||||
LoadingItem_()
|
// Avoid displaying two loaders if there is no elements between them
|
||||||
.id("backward_loading_item")
|
if (!loaderAdded || timelineModels.isNotEmpty()) {
|
||||||
.addWhen(Timeline.Direction.BACKWARDS)
|
LoadingItem_()
|
||||||
|
.id("backward_loading_item")
|
||||||
|
.addWhen(Timeline.Direction.BACKWARDS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeline.LISTENER ***************************************************************************
|
// Timeline.LISTENER ***************************************************************************
|
||||||
|
@ -310,9 +312,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun LoadingItem_.addWhen(direction: Timeline.Direction) {
|
/**
|
||||||
|
* Return true if added
|
||||||
|
*/
|
||||||
|
private fun LoadingItem_.addWhen(direction: Timeline.Direction): Boolean {
|
||||||
val shouldAdd = timeline?.hasMoreToLoad(direction) ?: false
|
val shouldAdd = timeline?.hasMoreToLoad(direction) ?: false
|
||||||
addIf(shouldAdd, this@TimelineEventController)
|
addIf(shouldAdd, this@TimelineEventController)
|
||||||
|
return shouldAdd
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchPositionOfEvent(eventId: String): Int? {
|
fun searchPositionOfEvent(eventId: String): Int? {
|
||||||
|
|
|
@ -126,51 +126,55 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
|
||||||
}
|
}
|
||||||
//TODO is downloading attachement?
|
//TODO is downloading attachement?
|
||||||
|
|
||||||
if (event.canReact()) {
|
if (!event.root.isRedacted()) {
|
||||||
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, eventId))
|
|
||||||
}
|
|
||||||
if (canCopy(type)) {
|
|
||||||
//TODO copy images? html? see ClipBoard
|
|
||||||
this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent!!.body))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canReply(event, messageContent)) {
|
if (canReply(event, messageContent)) {
|
||||||
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId))
|
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canEdit(event, session.sessionParams.credentials.userId)) {
|
if (canEdit(event, session.myUserId)) {
|
||||||
this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, eventId))
|
this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, eventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canRedact(event, session.sessionParams.credentials.userId)) {
|
if (canRedact(event, session.myUserId)) {
|
||||||
this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, eventId))
|
this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, eventId))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canQuote(event, messageContent)) {
|
if (canCopy(type)) {
|
||||||
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId))
|
//TODO copy images? html? see ClipBoard
|
||||||
}
|
this.add(SimpleAction(ACTION_COPY, R.string.copy, R.drawable.ic_copy, messageContent!!.body))
|
||||||
|
}
|
||||||
if (canViewReactions(event)) {
|
|
||||||
this.add(SimpleAction(ACTION_VIEW_REACTIONS, R.string.message_view_reaction, R.drawable.ic_view_reactions, informationData))
|
if (event.canReact()) {
|
||||||
}
|
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, eventId))
|
||||||
|
}
|
||||||
if (canShare(type)) {
|
|
||||||
if (messageContent is MessageImageContent) {
|
if (canQuote(event, messageContent)) {
|
||||||
this.add(
|
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId))
|
||||||
SimpleAction(ACTION_SHARE,
|
}
|
||||||
R.string.share, R.drawable.ic_share,
|
|
||||||
session.contentUrlResolver().resolveFullSize(messageContent.url))
|
if (canViewReactions(event)) {
|
||||||
)
|
this.add(SimpleAction(ACTION_VIEW_REACTIONS, R.string.message_view_reaction, R.drawable.ic_view_reactions, informationData))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canShare(type)) {
|
||||||
|
if (messageContent is MessageImageContent) {
|
||||||
|
this.add(
|
||||||
|
SimpleAction(ACTION_SHARE,
|
||||||
|
R.string.share, R.drawable.ic_share,
|
||||||
|
session.contentUrlResolver().resolveFullSize(messageContent.url))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
//TODO
|
||||||
}
|
}
|
||||||
//TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (event.sendState == SendState.SENT) {
|
if (event.sendState == SendState.SENT) {
|
||||||
|
|
||||||
//TODO Can be redacted
|
//TODO Can be redacted
|
||||||
|
|
||||||
//TODO sent by me or sufficient power level
|
//TODO sent by me or sufficient power level
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, event.root.toContentStringWithIndent()))
|
this.add(SimpleAction(VIEW_SOURCE, R.string.view_source, R.drawable.ic_view_source, event.root.toContentStringWithIndent()))
|
||||||
|
@ -181,7 +185,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
|
||||||
}
|
}
|
||||||
this.add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, event.root.eventId))
|
this.add(SimpleAction(ACTION_COPY_PERMALINK, R.string.permalink, R.drawable.ic_permalink, event.root.eventId))
|
||||||
|
|
||||||
if (session.sessionParams.credentials.userId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
|
if (session.myUserId != event.root.senderId && event.root.getClearType() == EventType.MESSAGE) {
|
||||||
//not sent by me
|
//not sent by me
|
||||||
this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, event.root.eventId))
|
this.add(SimpleAction(ACTION_FLAG, R.string.report_content, R.drawable.ic_flag, event.root.eventId))
|
||||||
}
|
}
|
||||||
|
@ -240,7 +244,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
|
||||||
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||||
//TODO if user is admin or moderator
|
//TODO if user is admin or moderator
|
||||||
val messageContent = event.root.content.toModel<MessageContent>()
|
val messageContent = event.root.getClearContent().toModel<MessageContent>()
|
||||||
return event.root.senderId == myUserId && (
|
return event.root.senderId == myUserId && (
|
||||||
messageContent?.type == MessageType.MSGTYPE_TEXT
|
messageContent?.type == MessageType.MSGTYPE_TEXT
|
||||||
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
|
|| messageContent?.type == MessageType.MSGTYPE_EMOTE
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* 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.LinearLayout
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import butterknife.BindView
|
||||||
|
import butterknife.ButterKnife
|
||||||
|
import com.airbnb.epoxy.EpoxyRecyclerView
|
||||||
|
import com.airbnb.mvrx.MvRx
|
||||||
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
|
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||||
|
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bottom sheet displaying list of edits for a given event ordered by timestamp
|
||||||
|
*/
|
||||||
|
class ViewEditHistoryBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
private val viewModel: ViewEditHistoryViewModel by fragmentViewModel(ViewEditHistoryViewModel::class)
|
||||||
|
|
||||||
|
@Inject lateinit var viewEditHistoryViewModelFactory: ViewEditHistoryViewModel.Factory
|
||||||
|
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
|
||||||
|
|
||||||
|
@BindView(R.id.bottom_sheet_display_reactions_list)
|
||||||
|
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
||||||
|
|
||||||
|
private val epoxyController by lazy {
|
||||||
|
ViewEditHistoryEpoxyController(requireContext(), viewModel.timelineDateFormatter, eventHtmlRenderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun injectWith(screenComponent: ScreenComponent) {
|
||||||
|
screenComponent.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
|
||||||
|
ButterKnife.bind(this, view)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
epoxyRecyclerView.setController(epoxyController)
|
||||||
|
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
|
||||||
|
LinearLayout.VERTICAL)
|
||||||
|
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
|
||||||
|
bottomSheetTitle.text = context?.getString(R.string.message_edits)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun invalidate() = withState(viewModel) {
|
||||||
|
epoxyController.setData(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun newInstance(roomId: String, informationData: MessageInformationData): ViewEditHistoryBottomSheet {
|
||||||
|
val args = Bundle()
|
||||||
|
val parcelableArgs = TimelineEventFragmentArgs(
|
||||||
|
informationData.eventId,
|
||||||
|
roomId,
|
||||||
|
informationData
|
||||||
|
)
|
||||||
|
args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
|
||||||
|
return ViewEditHistoryBottomSheet().apply { arguments = args }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
/*
|
||||||
|
* 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.content.Context
|
||||||
|
import android.text.Spannable
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import com.airbnb.mvrx.Fail
|
||||||
|
import com.airbnb.mvrx.Incomplete
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||||
|
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.localDateTime
|
||||||
|
import im.vector.riotx.core.ui.list.genericFooterItem
|
||||||
|
import im.vector.riotx.core.ui.list.genericItem
|
||||||
|
import im.vector.riotx.core.ui.list.genericItemHeader
|
||||||
|
import im.vector.riotx.core.ui.list.genericLoaderItem
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||||
|
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||||
|
import me.gujun.android.span.span
|
||||||
|
import name.fraser.neil.plaintext.diff_match_patch
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Epoxy controller for reaction event list
|
||||||
|
*/
|
||||||
|
class ViewEditHistoryEpoxyController(private val context: Context,
|
||||||
|
val timelineDateFormatter: TimelineDateFormatter,
|
||||||
|
val eventHtmlRenderer: EventHtmlRenderer) : TypedEpoxyController<ViewEditHistoryViewState>() {
|
||||||
|
|
||||||
|
override fun buildModels(state: ViewEditHistoryViewState) {
|
||||||
|
when (state.editList) {
|
||||||
|
is Incomplete -> {
|
||||||
|
genericLoaderItem {
|
||||||
|
id("Spinner")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Fail -> {
|
||||||
|
genericFooterItem {
|
||||||
|
id("failure")
|
||||||
|
text(context.getString(R.string.unknown_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Success -> {
|
||||||
|
state.editList()?.let { renderEvents(it, state.isOriginalAReply) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderEvents(sourceEvents: List<Event>, isOriginalReply: Boolean) {
|
||||||
|
if (sourceEvents.isEmpty()) {
|
||||||
|
genericItem {
|
||||||
|
id("footer")
|
||||||
|
title(context.getString(R.string.no_message_edits_found))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var lastDate: Calendar? = null
|
||||||
|
sourceEvents.forEachIndexed { index, timelineEvent ->
|
||||||
|
|
||||||
|
val evDate = Calendar.getInstance().apply {
|
||||||
|
timeInMillis = timelineEvent.originServerTs
|
||||||
|
?: System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) {
|
||||||
|
//need to display header with day
|
||||||
|
val dateString = if (DateUtils.isToday(evDate.timeInMillis)) context.getString(R.string.today)
|
||||||
|
else timelineDateFormatter.formatMessageDay(timelineEvent.localDateTime())
|
||||||
|
genericItemHeader {
|
||||||
|
id(evDate.hashCode())
|
||||||
|
text(dateString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastDate = evDate
|
||||||
|
val cContent = getCorrectContent(timelineEvent, isOriginalReply)
|
||||||
|
val body = cContent.second?.let { eventHtmlRenderer.render(it) }
|
||||||
|
?: cContent.first
|
||||||
|
|
||||||
|
val nextEvent = if (index + 1 <= sourceEvents.lastIndex) sourceEvents[index + 1] else null
|
||||||
|
|
||||||
|
var spannedDiff: Spannable? = null
|
||||||
|
if (nextEvent != null && cContent.second == null /*No diff for html*/) {
|
||||||
|
//compares the body
|
||||||
|
val nContent = getCorrectContent(nextEvent, isOriginalReply)
|
||||||
|
val nextBody = nContent.second?.let { eventHtmlRenderer.render(it) }
|
||||||
|
?: nContent.first
|
||||||
|
val dmp = diff_match_patch()
|
||||||
|
val diff = dmp.diff_main(nextBody.toString(), body.toString())
|
||||||
|
Timber.e("#### Diff: $diff")
|
||||||
|
dmp.diff_cleanupSemantic(diff)
|
||||||
|
Timber.e("#### Diff: $diff")
|
||||||
|
spannedDiff = span {
|
||||||
|
diff.map {
|
||||||
|
when (it.operation) {
|
||||||
|
diff_match_patch.Operation.DELETE -> {
|
||||||
|
span {
|
||||||
|
text = it.text
|
||||||
|
textColor = ContextCompat.getColor(context, R.color.vector_error_color)
|
||||||
|
textDecorationLine = "line-through"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diff_match_patch.Operation.INSERT -> {
|
||||||
|
span {
|
||||||
|
text = it.text
|
||||||
|
textColor = ContextCompat.getColor(context, R.color.vector_success_color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
span {
|
||||||
|
text = it.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
genericItem {
|
||||||
|
id(timelineEvent.eventId)
|
||||||
|
title(timelineDateFormatter.formatMessageHour(timelineEvent.localDateTime()))
|
||||||
|
description(spannedDiff ?: body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCorrectContent(event: Event, isOriginalReply: Boolean): Pair<String, String?> {
|
||||||
|
val clearContent = event.getClearContent().toModel<MessageTextContent>()
|
||||||
|
val newContent = clearContent
|
||||||
|
?.newContent
|
||||||
|
?.toModel<MessageTextContent>()
|
||||||
|
if (isOriginalReply) {
|
||||||
|
return extractUsefulTextFromReply(newContent?.body ?: clearContent?.body ?: "") to null
|
||||||
|
}
|
||||||
|
return (newContent?.body ?: clearContent?.body ?: "") to (newContent?.formattedBody
|
||||||
|
?: clearContent?.formattedBody)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
* 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.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
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.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.isReply
|
||||||
|
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
data class ViewEditHistoryViewState(
|
||||||
|
val eventId: String,
|
||||||
|
val roomId: String,
|
||||||
|
val isOriginalAReply: Boolean = false,
|
||||||
|
val editList: Async<List<Event>> = Uninitialized)
|
||||||
|
: MvRxState {
|
||||||
|
|
||||||
|
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
|
||||||
|
initialState: ViewEditHistoryViewState,
|
||||||
|
val session: Session,
|
||||||
|
val timelineDateFormatter: TimelineDateFormatter
|
||||||
|
) : VectorViewModel<ViewEditHistoryViewState>(initialState) {
|
||||||
|
|
||||||
|
private val roomId = initialState.roomId
|
||||||
|
private val eventId = initialState.eventId
|
||||||
|
private val room = session.getRoom(roomId)
|
||||||
|
?: throw IllegalStateException("Shouldn't use this ViewModel without a room")
|
||||||
|
|
||||||
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: ViewEditHistoryViewState): ViewEditHistoryViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<ViewEditHistoryViewModel, ViewEditHistoryViewState> {
|
||||||
|
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: ViewEditHistoryViewState): ViewEditHistoryViewModel? {
|
||||||
|
val fragment: ViewEditHistoryBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
|
return fragment.viewEditHistoryViewModelFactory.create(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadHistory() {
|
||||||
|
setState { copy(editList = Loading()) }
|
||||||
|
room.fetchEditHistory(eventId, object : MatrixCallback<List<Event>> {
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
setState {
|
||||||
|
copy(editList = Fail(failure))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: List<Event>) {
|
||||||
|
var originalIsReply = false
|
||||||
|
|
||||||
|
val events = data.map { event ->
|
||||||
|
val timelineID = event.roomId + UUID.randomUUID().toString()
|
||||||
|
event.also {
|
||||||
|
//We need to check encryption
|
||||||
|
if (it.isEncrypted() && it.mxDecryptionResult == null) {
|
||||||
|
//for now decrypt sync
|
||||||
|
try {
|
||||||
|
val result = session.decryptEvent(it, timelineID)
|
||||||
|
it.mxDecryptionResult = OlmDecryptionResult(
|
||||||
|
payload = result.clearEvent,
|
||||||
|
senderKey = result.senderCurve25519Key,
|
||||||
|
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||||
|
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||||
|
)
|
||||||
|
} catch (e: MXCryptoError) {
|
||||||
|
Timber.w("Failed to decrypt event in history")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.eventId == it.eventId) {
|
||||||
|
originalIsReply = it.getClearContent().toModel<MessageContent>().isReply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
editList = Success(events),
|
||||||
|
isOriginalAReply = originalIsReply
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -21,7 +21,6 @@ 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 android.widget.LinearLayout
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import butterknife.BindView
|
import butterknife.BindView
|
||||||
import butterknife.ButterKnife
|
import butterknife.ButterKnife
|
||||||
|
@ -33,7 +32,7 @@ import im.vector.riotx.EmojiCompatFontProvider
|
||||||
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.item.MessageInformationData
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import kotlinx.android.synthetic.main.bottom_sheet_display_reactions.*
|
import kotlinx.android.synthetic.main.bottom_sheet_epoxylist_with_title.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,14 +48,16 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
@BindView(R.id.bottom_sheet_display_reactions_list)
|
@BindView(R.id.bottom_sheet_display_reactions_list)
|
||||||
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
lateinit var epoxyRecyclerView: EpoxyRecyclerView
|
||||||
|
|
||||||
private val epoxyController by lazy { ViewReactionsEpoxyController(emojiCompatFontProvider.typeface) }
|
private val epoxyController by lazy {
|
||||||
|
ViewReactionsEpoxyController(requireContext(), emojiCompatFontProvider.typeface)
|
||||||
|
}
|
||||||
|
|
||||||
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_display_reactions, container, false)
|
val view = inflater.inflate(R.layout.bottom_sheet_epoxylist_with_title, container, false)
|
||||||
ButterKnife.bind(this, view)
|
ButterKnife.bind(this, view)
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
@ -67,16 +68,11 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
|
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
|
||||||
LinearLayout.VERTICAL)
|
LinearLayout.VERTICAL)
|
||||||
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
|
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
|
||||||
|
bottomSheetTitle.text = context?.getString(R.string.reactions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) {
|
override fun invalidate() = withState(viewModel) {
|
||||||
if (it.mapReactionKeyToMemberList() == null) {
|
|
||||||
bottomSheetViewReactionSpinner.isVisible = true
|
|
||||||
bottomSheetViewReactionSpinner.animate()
|
|
||||||
} else {
|
|
||||||
bottomSheetViewReactionSpinner.isVisible = false
|
|
||||||
}
|
|
||||||
epoxyController.setData(it)
|
epoxyController.setData(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,24 +16,47 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail.timeline.action
|
package im.vector.riotx.features.home.room.detail.timeline.action
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import com.airbnb.epoxy.TypedEpoxyController
|
import com.airbnb.epoxy.TypedEpoxyController
|
||||||
|
import com.airbnb.mvrx.Fail
|
||||||
|
import com.airbnb.mvrx.Incomplete
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.ui.list.genericFooterItem
|
||||||
|
import im.vector.riotx.core.ui.list.genericLoaderItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Epoxy controller for reaction event list
|
* Epoxy controller for reaction event list
|
||||||
*/
|
*/
|
||||||
class ViewReactionsEpoxyController(private val emojiCompatTypeface: Typeface?) : TypedEpoxyController<DisplayReactionsViewState>() {
|
class ViewReactionsEpoxyController(private val context: Context, private val emojiCompatTypeface: Typeface?)
|
||||||
|
: TypedEpoxyController<DisplayReactionsViewState>() {
|
||||||
|
|
||||||
override fun buildModels(state: DisplayReactionsViewState) {
|
override fun buildModels(state: DisplayReactionsViewState) {
|
||||||
val map = state.mapReactionKeyToMemberList() ?: return
|
when (state.mapReactionKeyToMemberList) {
|
||||||
map.forEach {
|
is Incomplete -> {
|
||||||
reactionInfoSimpleItem {
|
genericLoaderItem {
|
||||||
id(it.eventId)
|
id("Spinner")
|
||||||
emojiTypeFace(emojiCompatTypeface)
|
}
|
||||||
timeStamp(it.timestamp)
|
}
|
||||||
reactionKey(it.reactionKey)
|
is Fail -> {
|
||||||
authorDisplayName(it.authorName ?: it.authorId)
|
genericFooterItem {
|
||||||
|
id("failure")
|
||||||
|
text(context.getString(R.string.unknown_error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Success -> {
|
||||||
|
state.mapReactionKeyToMemberList()?.forEach {
|
||||||
|
reactionInfoSimpleItem {
|
||||||
|
id(it.eventId)
|
||||||
|
emojiTypeFace(emojiCompatTypeface)
|
||||||
|
timeStamp(it.timestamp)
|
||||||
|
reactionKey(it.reactionKey)
|
||||||
|
authorDisplayName(it.authorName ?: it.authorId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -68,6 +68,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.message(spannableStr)
|
.message(spannableStr)
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
|
.colorProvider(colorProvider)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
|
|
|
@ -27,12 +27,14 @@ import dagger.Lazy
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
|
||||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
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.internal.crypto.attachments.toElementToDecrypt
|
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.riotx.EmojiCompatFontProvider
|
import im.vector.riotx.EmojiCompatFontProvider
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
@ -83,7 +85,9 @@ class MessageItemFactory @Inject constructor(
|
||||||
?: //Malformed content, we should echo something on screen
|
?: //Malformed content, we should echo something on screen
|
||||||
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
|
||||||
|
|
||||||
if (messageContent.relatesTo?.type == RelationType.REPLACE) {
|
if (messageContent.relatesTo?.type == RelationType.REPLACE
|
||||||
|
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|
||||||
|
) {
|
||||||
// ignore replace event, the targeted id is already edited
|
// ignore replace event, the targeted id is already edited
|
||||||
return BlankItem_()
|
return BlankItem_()
|
||||||
}
|
}
|
||||||
|
@ -117,6 +121,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
callback: TimelineEventController.Callback?): MessageFileItem? {
|
callback: TimelineEventController.Callback?): MessageFileItem? {
|
||||||
return MessageFileItem_()
|
return MessageFileItem_()
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
|
.colorProvider(colorProvider)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
|
@ -144,6 +149,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
callback: TimelineEventController.Callback?): MessageFileItem? {
|
callback: TimelineEventController.Callback?): MessageFileItem? {
|
||||||
return MessageFileItem_()
|
return MessageFileItem_()
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
|
.colorProvider(colorProvider)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
|
@ -195,6 +201,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
)
|
)
|
||||||
return MessageImageVideoItem_()
|
return MessageImageVideoItem_()
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
|
.colorProvider(colorProvider)
|
||||||
.imageContentRenderer(imageContentRenderer)
|
.imageContentRenderer(imageContentRenderer)
|
||||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||||
.playable(messageContent.info?.mimeType == "image/gif")
|
.playable(messageContent.info?.mimeType == "image/gif")
|
||||||
|
@ -226,7 +233,8 @@ class MessageItemFactory @Inject constructor(
|
||||||
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
|
||||||
val thumbnailData = ImageContentRenderer.Data(
|
val thumbnailData = ImageContentRenderer.Data(
|
||||||
filename = messageContent.body,
|
filename = messageContent.body,
|
||||||
url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
|
url = messageContent.videoInfo?.thumbnailFile?.url
|
||||||
|
?: messageContent.videoInfo?.thumbnailUrl,
|
||||||
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
|
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
|
||||||
height = messageContent.videoInfo?.height,
|
height = messageContent.videoInfo?.height,
|
||||||
maxHeight = maxHeight,
|
maxHeight = maxHeight,
|
||||||
|
@ -246,6 +254,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
.imageContentRenderer(imageContentRenderer)
|
.imageContentRenderer(imageContentRenderer)
|
||||||
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
|
.colorProvider(colorProvider)
|
||||||
.playable(true)
|
.playable(true)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
|
@ -288,6 +297,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
|
.colorProvider(colorProvider)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
.urlClickCallback(callback)
|
.urlClickCallback(callback)
|
||||||
|
@ -353,6 +363,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
.message(message)
|
.message(message)
|
||||||
|
.colorProvider(colorProvider)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
|
@ -393,6 +404,7 @@ class MessageItemFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
|
.colorProvider(colorProvider)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
|
@ -414,9 +426,18 @@ class MessageItemFactory @Inject constructor(
|
||||||
callback: TimelineEventController.Callback?): RedactedMessageItem? {
|
callback: TimelineEventController.Callback?): RedactedMessageItem? {
|
||||||
return RedactedMessageItem_()
|
return RedactedMessageItem_()
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
|
.colorProvider(colorProvider)
|
||||||
.informationData(informationData)
|
.informationData(informationData)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.avatarCallback(callback)
|
.avatarCallback(callback)
|
||||||
|
.cellClickListener(
|
||||||
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
callback?.onEventCellClicked(informationData, null, view)
|
||||||
|
}))
|
||||||
|
.longClickListener { view ->
|
||||||
|
return@longClickListener callback?.onEventLongClicked(informationData, null, view)
|
||||||
|
?: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence {
|
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence {
|
||||||
|
|
|
@ -23,12 +23,16 @@ import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
import im.vector.riotx.features.media.ImageContentRenderer
|
import im.vector.riotx.features.media.ImageContentRenderer
|
||||||
|
import im.vector.riotx.features.ui.getMessageTextColor
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) {
|
class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
||||||
|
private val colorProvider: ColorProvider) {
|
||||||
|
|
||||||
private val updateListeners = mutableMapOf<String, ContentUploadStateTracker.UpdateListener>()
|
private val updateListeners = mutableMapOf<String, ContentUploadStateTracker.UpdateListener>()
|
||||||
|
|
||||||
|
@ -38,7 +42,7 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
|
||||||
|
|
||||||
activeSessionHolder.getActiveSession().also { session ->
|
activeSessionHolder.getActiveSession().also { session ->
|
||||||
val uploadStateTracker = session.contentUploadProgressTracker()
|
val uploadStateTracker = session.contentUploadProgressTracker()
|
||||||
val updateListener = ContentMediaProgressUpdater(progressLayout, mediaData)
|
val updateListener = ContentMediaProgressUpdater(progressLayout, mediaData, colorProvider)
|
||||||
updateListeners[eventId] = updateListener
|
updateListeners[eventId] = updateListener
|
||||||
uploadStateTracker.track(eventId, updateListener)
|
uploadStateTracker.track(eventId, updateListener)
|
||||||
}
|
}
|
||||||
|
@ -56,7 +60,8 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
||||||
private val mediaData: ImageContentRenderer.Data) : ContentUploadStateTracker.UpdateListener {
|
private val mediaData: ImageContentRenderer.Data,
|
||||||
|
private val colorProvider: ColorProvider) : ContentUploadStateTracker.UpdateListener {
|
||||||
|
|
||||||
override fun onUpdate(state: ContentUploadStateTracker.State) {
|
override fun onUpdate(state: ContentUploadStateTracker.State) {
|
||||||
when (state) {
|
when (state) {
|
||||||
|
@ -79,6 +84,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
||||||
progressBar?.isIndeterminate = true
|
progressBar?.isIndeterminate = true
|
||||||
progressBar?.progress = 0
|
progressBar?.progress = 0
|
||||||
progressTextView?.text = progressLayout.context.getString(R.string.send_file_step_idle)
|
progressTextView?.text = progressLayout.context.getString(R.string.send_file_step_idle)
|
||||||
|
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.UNSENT))
|
||||||
} else {
|
} else {
|
||||||
progressLayout.isVisible = false
|
progressLayout.isVisible = false
|
||||||
}
|
}
|
||||||
|
@ -106,6 +112,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
||||||
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
|
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
|
||||||
progressBar?.isIndeterminate = true
|
progressBar?.isIndeterminate = true
|
||||||
progressTextView?.text = progressLayout.context.getString(resId)
|
progressTextView?.text = progressLayout.context.getString(resId)
|
||||||
|
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.ENCRYPTING))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doHandleProgress(resId: Int, current: Long, total: Long) {
|
private fun doHandleProgress(resId: Int, current: Long, total: Long) {
|
||||||
|
@ -119,6 +126,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
||||||
progressTextView?.text = progressLayout.context.getString(resId,
|
progressTextView?.text = progressLayout.context.getString(resId,
|
||||||
Formatter.formatShortFileSize(progressLayout.context, current),
|
Formatter.formatShortFileSize(progressLayout.context, current),
|
||||||
Formatter.formatShortFileSize(progressLayout.context, total))
|
Formatter.formatShortFileSize(progressLayout.context, total))
|
||||||
|
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.SENDING))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFailure(state: ContentUploadStateTracker.State.Failure) {
|
private fun handleFailure(state: ContentUploadStateTracker.State.Failure) {
|
||||||
|
@ -126,8 +134,8 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
|
||||||
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
|
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
|
||||||
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
|
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
|
||||||
progressBar?.isVisible = false
|
progressBar?.isVisible = false
|
||||||
// TODO Red text
|
|
||||||
progressTextView?.text = state.throwable.localizedMessage
|
progressTextView?.text = state.throwable.localizedMessage
|
||||||
|
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.UNDELIVERED))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSuccess(state: ContentUploadStateTracker.State.Success) {
|
private fun handleSuccess(state: ContentUploadStateTracker.State.Success) {
|
||||||
|
|
|
@ -23,24 +23,32 @@ import android.view.ViewGroup
|
||||||
import android.view.ViewStub
|
import android.view.ViewStub
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.IdRes
|
||||||
import androidx.constraintlayout.helper.widget.Flow
|
import androidx.constraintlayout.helper.widget.Flow
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||||
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
|
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
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.reactions.widget.ReactionButton
|
import im.vector.riotx.features.reactions.widget.ReactionButton
|
||||||
|
import im.vector.riotx.features.ui.getMessageTextColor
|
||||||
|
|
||||||
|
|
||||||
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||||
|
|
||||||
abstract val informationData: MessageInformationData
|
@EpoxyAttribute
|
||||||
|
lateinit var informationData: MessageInformationData
|
||||||
|
|
||||||
abstract val avatarRenderer: AvatarRenderer
|
@EpoxyAttribute
|
||||||
|
lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
lateinit var colorProvider: ColorProvider
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var longClickListener: View.OnLongClickListener? = null
|
var longClickListener: View.OnLongClickListener? = null
|
||||||
|
@ -153,13 +161,12 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun View.renderSendState() {
|
protected fun renderSendState(root: View, textView: TextView?) {
|
||||||
isClickable = informationData.sendState.isSent()
|
root.isClickable = informationData.sendState.isSent()
|
||||||
alpha = if (informationData.sendState.isSent()) 1f else 0.5f
|
textView?.setTextColor(colorProvider.getMessageTextColor(informationData.sendState))
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class Holder : BaseHolder() {
|
abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) {
|
||||||
|
|
||||||
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||||
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||||
|
|
|
@ -26,6 +26,9 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
import im.vector.riotx.core.platform.CheckableView
|
import im.vector.riotx.core.platform.CheckableView
|
||||||
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
|
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Children must override getViewType()
|
||||||
|
*/
|
||||||
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
|
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
|
||||||
|
|
||||||
var avatarStyle: AvatarStyle = AvatarStyle.SMALL
|
var avatarStyle: AvatarStyle = AvatarStyle.SMALL
|
||||||
|
@ -43,31 +46,18 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
||||||
holder.checkableBackground.isChecked = highlighted
|
holder.checkableBackground.isChecked = highlighted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
|
||||||
override fun getViewType(): Int {
|
|
||||||
return getStubType()
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun getStubType(): Int
|
|
||||||
|
|
||||||
|
|
||||||
abstract class BaseHolder : VectorEpoxyHolder() {
|
|
||||||
|
|
||||||
val leftGuideline by bind<Guideline>(R.id.messageStartGuideline)
|
val leftGuideline by bind<Guideline>(R.id.messageStartGuideline)
|
||||||
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
||||||
|
|
||||||
@IdRes
|
|
||||||
abstract fun getStubId(): Int
|
|
||||||
|
|
||||||
override fun bindView(itemView: View) {
|
override fun bindView(itemView: View) {
|
||||||
super.bindView(itemView)
|
super.bindView(itemView)
|
||||||
inflateStub()
|
inflateStub()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun inflateStub() {
|
private fun inflateStub() {
|
||||||
view.findViewById<ViewStub>(getStubId()).inflate()
|
view.findViewById<ViewStub>(stubId).inflate()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -31,11 +31,9 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
|
||||||
holder.messageView.text = text
|
holder.messageView.text = text
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStubType(): Int = STUB_ID
|
override fun getViewType() = STUB_ID
|
||||||
|
|
||||||
class Holder : BaseHolder() {
|
|
||||||
override fun getStubId(): Int = STUB_ID
|
|
||||||
|
|
||||||
|
class Holder : BaseHolder(STUB_ID) {
|
||||||
val messageView by bind<TextView>(R.id.stateMessageView)
|
val messageView by bind<TextView>(R.id.stateMessageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ data class MergedHeaderItem(private val isCollapsed: Boolean,
|
||||||
return Holder()
|
return Holder()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStubType(): Int = STUB_ID
|
override fun getViewType() = STUB_ID
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
|
@ -84,8 +84,7 @@ data class MergedHeaderItem(private val isCollapsed: Boolean,
|
||||||
val avatarUrl: String?
|
val avatarUrl: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
class Holder : BaseHolder() {
|
class Holder : BaseHolder(STUB_ID) {
|
||||||
override fun getStubId(): Int = STUB_ID
|
|
||||||
|
|
||||||
val expandView by bind<TextView>(R.id.itemMergedExpandTextView)
|
val expandView by bind<TextView>(R.id.itemMergedExpandTextView)
|
||||||
val summaryView by bind<TextView>(R.id.itemMergedSummaryTextView)
|
val summaryView by bind<TextView>(R.id.itemMergedSummaryTextView)
|
||||||
|
@ -95,6 +94,6 @@ data class MergedHeaderItem(private val isCollapsed: Boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val STUB_ID = R.id.messageContentMergedheaderStub
|
private const val STUB_ID = R.id.messageContentMergedheaderStub
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -25,7 +25,6 @@ import androidx.annotation.DrawableRes
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||||
|
@ -36,34 +35,27 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
var iconRes: Int = 0
|
var iconRes: Int = 0
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
override lateinit var informationData: MessageInformationData
|
|
||||||
@EpoxyAttribute
|
|
||||||
override lateinit var avatarRenderer: AvatarRenderer
|
|
||||||
@EpoxyAttribute
|
|
||||||
var clickListener: View.OnClickListener? = null
|
var clickListener: View.OnClickListener? = null
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
holder.fileLayout.renderSendState()
|
renderSendState(holder.fileLayout, holder.filenameView)
|
||||||
holder.filenameView.text = filename
|
holder.filenameView.text = filename
|
||||||
holder.fileImageView.setImageResource(iconRes)
|
holder.fileImageView.setImageResource(iconRes)
|
||||||
holder.filenameView.setOnClickListener(clickListener)
|
holder.filenameView.setOnClickListener(clickListener)
|
||||||
holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG)
|
holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStubType(): Int = STUB_ID
|
override fun getViewType() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder() {
|
|
||||||
override fun getStubId(): Int = STUB_ID
|
|
||||||
|
|
||||||
|
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||||
val fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
|
val fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
|
||||||
val fileImageView by bind<ImageView>(R.id.messageFileImageView)
|
val fileImageView by bind<ImageView>(R.id.messageFileImageView)
|
||||||
val filenameView by bind<TextView>(R.id.messageFilenameView)
|
val filenameView by bind<TextView>(R.id.messageFilenameView)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val STUB_ID = R.id.messageContentFileStub
|
private const val STUB_ID = R.id.messageContentFileStub
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue