Compare commits

..

89 Commits

Author SHA1 Message Date
df6080b1da Merge branch 'release/0.2.0' 2019-07-18 17:47:39 +02:00
d609c49b31 Prepare release 0.2.0 2019-07-18 17:47:24 +02:00
d87ee32422 Merge pull request #384 from vector-im/feature/edit_e2e
Feature/edit e2e
2019-07-18 16:44:44 +02:00
f0671b9e73 "Riot X" -> "RiotX" 2019-07-18 14:28:46 +02:00
e218691bf2 Import strings and translation from Riot 2019-07-18 14:25:34 +02:00
9c67036c08 Fix / keyboard won't show when using reply from long tap menu 2019-07-18 12:13:17 +02:00
62657538af Merge pull request #389 from vector-im/feature/cleanup
Do not show invitation in the filtered room list
2019-07-18 12:10:47 +02:00
5438207fba faster animation for quick reply 2019-07-18 12:01:23 +02:00
fe88aaffbd Inject RoomListNameFilter 2019-07-18 11:39:13 +02:00
21ba72e5e7 Do not show invitation in the filtered room list 2019-07-18 11:34:49 +02:00
d48ae967bd Remove dead code 2019-07-18 11:11:42 +02:00
0afde3b021 Rename class member for code clarity 2019-07-18 11:07:09 +02:00
49ae954183 Merge remote-tracking branch 'origin/develop' into develop 2019-07-18 10:58:40 +02:00
64bee91f7a Merge pull request #387 from vector-im/feature/fix_sync_state
Fix sync state progress bar
2019-07-18 10:57:46 +02:00
51fdccb393 cleaning 2019-07-18 09:29:27 +02:00
7e3b300130 Fix sync state progress bar 2019-07-17 19:45:35 +02:00
a98b324c89 Merge pull request #385 from vector-im/feature/invit_notif
Cancel invitation notification when handling the invitation in the application
2019-07-17 18:39:40 +02:00
977721881f Cancel invitation notification when handling the invitation in the application 2019-07-17 18:35:41 +02:00
7d41352918 Fix / edit reply was quoting wrong text
+ e2e reply of edit
2019-07-17 16:46:56 +02:00
077396a832 E2E replies
+ Edit History / support e2e and use original event
2019-07-17 16:20:12 +02:00
32b79bd50e Remove extra space around userId 2019-07-17 15:13:12 +02:00
844f6d16a4 Code quality 2019-07-17 15:05:29 +02:00
fc9ef579ca Merge pull request #381 from vector-im/feature/room_members_perf
Feature/room members perf
2019-07-17 15:01:06 +02:00
77fa5af1b8 Fix compilation issue after merge 2019-07-17 14:58:23 +02:00
90d25ff45e Code cleanup 2019-07-17 14:41:01 +02:00
173452d38c Merge pull request #367 from Dominaezzz/kotlinify-3
Some more kotlinification.
2019-07-17 14:38:16 +02:00
a9f9083745 Merge pull request #374 from vector-im/feature/quick_fix_long_click_link
WIP /  Fix Copying link from a message shouldn't open context menu
2019-07-17 14:37:11 +02:00
22dc2a6790 Fix Copying link from a message shouldn't open context menu 2019-07-17 14:36:47 +02:00
927cd7285d Merge pull request #378 from vector-im/feature/fix_sync_thread_wrong_autostart
Fix / SyncThread was started in background
2019-07-17 14:32:19 +02:00
4d5bdecec6 Merge pull request #382 from vector-im/feature/better_long_tap_menu
Feature/better long tap menu
2019-07-17 14:28:51 +02:00
0be987ac0d Merge branch 'develop' into feature/better_long_tap_menu 2019-07-17 14:28:36 +02:00
4bfaa00be4 Fix / clean bad method name 2019-07-17 14:27:02 +02:00
8e78d8a58d Merge pull request #380 from vector-im/feature/rs_crash_steve
Fix a crash in notificationwhen display name is empty
2019-07-17 14:22:45 +02:00
e3e86c0a41 Merge pull request #383 from vector-im/feature/filter_params
Pass filter to room directory screen or create room screen
2019-07-17 14:20:29 +02:00
8a5fddd952 Merge pull request #379 from vector-im/feature/small_fixes
Fix bad View used for searching in room directory.
2019-07-17 14:19:37 +02:00
208460850e Dagger: activate incremental build 2019-07-17 14:16:20 +02:00
0ddef67cc9 Migrate to rxbinding 3 and fix bad layout for room directory filter (Fixes #349) 2019-07-17 14:16:20 +02:00
896e582a9c Create style VectorSearchView 2019-07-17 14:16:20 +02:00
477920f411 Add some comment 2019-07-17 14:14:02 +02:00
c647648e79 Merge pull request #371 from vector-im/feature/composer_fix_edit_reply
Feature/composer fix edit reply
2019-07-17 14:03:10 +02:00
b654025a3b Fix alignment issue in toolbars 2019-07-17 12:38:35 +02:00
786a7d7560 Rename id 2019-07-17 12:20:11 +02:00
b935b9311e Scroll the list to top after each new filter 2019-07-17 12:18:45 +02:00
8e12f71535 Add top left back button 2019-07-17 12:16:10 +02:00
7eea2ccfb4 Fix infinite opening of room once the room is created 2019-07-17 12:09:09 +02:00
c32ef02a12 Pre fill the room directory filter and and the room name with the already entered string from the user 2019-07-17 12:04:19 +02:00
3651ec4870 Add some doc 2019-07-17 11:58:18 +02:00
87de7bd3e6 fix lint code quality 2019-07-17 11:41:14 +02:00
9494174c33 Swipe to reply in timeline (lab) 2019-07-17 10:54:15 +02:00
9bdea5b325 Change order of actions (and reply on top) 2019-07-16 16:35:57 +02:00
2f01ad99b3 Compact long tap menu 2019-07-16 16:35:36 +02:00
bb3b5788ba Update hint from design 2019-07-16 16:35:10 +02:00
45f7d3e9c4 Kotlin style 2019-07-16 15:59:08 +02:00
0f7a56d005 Use Session.myUserId whereas it's possible 2019-07-16 15:54:00 +02:00
63d2861bc8 Fix / SyncThread was started in background
Upon reception of a push, is the session is instantiated the sync thread was starting to loop
2019-07-16 15:44:08 +02:00
6bbc784c29 Fix crash (from Steve's rageshake) 2019-07-16 15:42:02 +02:00
c6fd625761 code review 2019-07-16 14:56:16 +02:00
d8092abc4e fix / strip reply prefix on history 2019-07-16 14:39:46 +02:00
6effb90361 Fix / edit of reply and edit of edit of reply 2019-07-16 14:39:05 +02:00
42584fc55a Merge pull request #372 from vector-im/feature/room_filtering
Room filtering
2019-07-16 11:41:08 +02:00
30d9ddb3e8 Merge pull request #373 from vector-im/feature/fix_composer_separator_dark
Fix / composer separator color was using a clear theme color
2019-07-15 18:16:06 +02:00
020c32bb1a Fix / composer separator color was using a clear theme color 2019-07-15 17:46:24 +02:00
efd973208f Green close icon 2019-07-15 17:35:51 +02:00
30a6c98c08 Room name in bold 2019-07-15 17:29:37 +02:00
1440080d04 Changes 2019-07-15 17:27:49 +02:00
61bb4c0427 Introduce CreateRoomActivity, a simple container for [CreateRoomFragment] 2019-07-15 17:26:48 +02:00
3c25088243 Filter rooms 2019-07-15 17:26:48 +02:00
fc1c0caea3 Avoid displaying two loaders if there is no elements between them 2019-07-15 17:25:59 +02:00
8901a5e09a Merge pull request #342 from vector-im/feature/edit_history
Feature/edit history
2019-07-15 15:15:45 +02:00
25f1d21bc7 Edit history
Get history from API


cleaning


Updated change log


Missing copyrights


Code review


cleaning
2019-07-15 14:57:12 +02:00
4d2ab9fa31 Merge pull request #344 from vector-im/feature/play_store_crash
Feature/play store crash
2019-07-15 10:49:20 +02:00
0289d2ee87 Simpler code 2019-07-15 10:48:44 +02:00
222201cc64 Fix crash observe on the PlayStore (#341) 2019-07-15 10:48:44 +02:00
b15dea6de3 Merge pull request #338 from vector-im/feature/green_encrypt
Text in green when encrypting
2019-07-15 10:46:44 +02:00
2ba83e456d Merge pull request #343 from vector-im/feature/click_on_redacted_event
Handle click on redacted event
2019-07-15 10:46:06 +02:00
1822fc4fbb Some more kotlinification
Signed-off-by: Dominic Fischer <dominicfischer7@gmail.com>
2019-07-13 15:35:10 +01:00
e6dd1fbfec Use GlobalScope instead of temp scope
Signed-off-by: Dominic Fischer <dominicfischer7@gmail.com>
2019-07-13 15:18:16 +01:00
e2ea76f871 Fix crash reported by PlayStore 2019-07-12 16:48:35 +02:00
34d14eb304 Fix regression on permalink click 2019-07-12 13:51:37 +02:00
3625c462f0 Click on redacted event 2019-07-12 13:51:37 +02:00
fe69206340 Prepare next release 2019-07-12 11:39:26 +02:00
f9885fd04c Update CHANGES.md 2019-07-12 11:38:55 +02:00
316c8ec27e Merge pull request #265 from vector-im/readme_update_for_beta
README: Update it for the beta launch
2019-07-12 11:36:49 +02:00
41465450d8 Code cleanup 2019-07-12 10:45:08 +02:00
bd009caaf1 Code cleanup 2019-07-12 10:22:58 +02:00
33252c3b65 Green text color during encrypting 2019-07-12 10:16:43 +02:00
4b971a9e67 README: Fix develop build links 2019-07-03 10:04:35 +02:00
698fc35704 README: Put back link to #riotx:matrix.org 2019-07-02 18:41:28 +02:00
1c69d8e425 README: Update it for the beta launch 2019-07-02 18:06:43 +02:00
146 changed files with 2754 additions and 670 deletions

View File

@ -1,11 +1,34 @@
Changes in RiotX 0.2.0 (2019-07-18)
===================================================
Features:
- Message Editing: View edit history (#121)
- Rooms filtering (#304)
- Edit in encrypted room
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:
- migrate from rxbinding 2 to rxbinding 3
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)
Changes in RiotX 0.1.0 (2019-07-11)
===================================================
First release!
Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-android-b17952e8f771
=======================================================

View File

@ -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)
[![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)
@ -7,14 +7,31 @@
# 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
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).

View File

@ -1,3 +1,5 @@
import javax.tools.JavaCompiler
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
@ -45,7 +47,26 @@ allprojects {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
google()
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) {

View File

@ -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
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.
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
@ -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!
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.
@ -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 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

View File

@ -42,7 +42,7 @@ object MatrixLinkify {
hasMatch = true
val startPos = match.range.first
if (startPos == 0 || text[startPos - 1] != '/') {
val endPos = match.range.last
val endPos = match.range.last + 1
val url = text.substring(match.range)
val span = MatrixPermalinkSpan(url, callback)
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
@ -51,5 +51,5 @@ object MatrixLinkify {
}
return hasMatch
}
}

View File

@ -57,6 +57,9 @@ interface Session :
*/
val sessionParams: SessionParams
/**
* Useful shortcut to get access to the userId
*/
val myUserId: String
get() = sessionParams.credentials.userId
@ -84,7 +87,7 @@ interface Session :
/**
* This method start the sync thread.
*/
fun startSync()
fun startSync(fromForeground : Boolean)
/**
* This method stop the sync thread.

View File

@ -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 {
@ -25,7 +25,7 @@ object RelationType {
const val ANNOTATION = "m.annotation"
/** Lets you define an event which replaces an existing event.*/
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"
}

View File

@ -25,4 +25,9 @@ interface MessageContent {
val body: String
val relatesTo: RelationDefaultContent?
val newContent: Content?
}
fun MessageContent?.isReply(): Boolean {
return this?.relatesTo?.inReplyTo != null
}

View File

@ -16,7 +16,10 @@
package im.vector.matrix.android.api.session.room.model.relation
import im.vector.matrix.android.api.session.events.model.RelationType
interface RelationContent {
/** See [RelationType] for known possible values */
val type: String?
val eventId: String?
val inReplyTo: ReplyToContent?

View File

@ -16,6 +16,8 @@
package im.vector.matrix.android.api.session.room.model.relation
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.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Cancelable
@ -79,6 +81,25 @@ interface RelationService {
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)
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
@ -91,4 +112,6 @@ interface RelationService {
autoMarkdown: Boolean = false): Cancelable?
fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary>
}

View File

@ -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.room.model.EventAnnotationsSummary
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.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.
@ -88,3 +91,15 @@ data class TimelineEvent(
*/
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.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 ?: ""
}
}

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.api.session.sync
sealed class SyncState {
object IDLE : SyncState()
data class RUNNING(val catchingUp: Boolean) : SyncState()
data class RUNNING(val afterPause: Boolean) : SyncState()
object PAUSED : SyncState()
object KILLING : SyncState()
object KILLED : SyncState()

View File

@ -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
}
}

View File

@ -50,16 +50,10 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
}
private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
val userId = sessionParams.credentials.userId
if (sessionComponents.containsKey(userId)) {
return sessionComponents[userId]!!
return sessionComponents.getOrPut(sessionParams.credentials.userId) {
DaggerSessionComponent
.factory()
.create(matrixComponent, sessionParams)
}
return DaggerSessionComponent
.factory()
.create(matrixComponent, sessionParams)
.also {
sessionComponents[sessionParams.credentials.userId] = it
}
}
}

View File

@ -21,7 +21,6 @@ package im.vector.matrix.android.internal.crypto
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import arrow.core.Try
import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy
@ -80,10 +79,9 @@ import im.vector.matrix.android.internal.util.fetchCopied
import kotlinx.coroutines.*
import org.matrix.olm.OlmManager
import timber.log.Timber
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
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.
@ -248,7 +246,7 @@ internal class CryptoManager @Inject constructor(
return
}
isStarting.set(true)
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
internalStart(isInitialSync)
}
}
@ -315,7 +313,7 @@ internal class CryptoManager @Inject constructor(
* @param syncResponse the syncResponse
*/
fun onSyncCompleted(syncResponse: SyncResponse) {
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
if (syncResponse.deviceLists != null) {
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
*/
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
null
} else cryptoStore.deviceWithIdentityKey(senderKey)
@ -353,8 +351,8 @@ internal class CryptoManager @Inject constructor(
* @param deviceId the device id
*/
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
return if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) {
cryptoStore.getUserDevice(deviceId!!, userId)
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
cryptoStore.getUserDevice(deviceId, userId)
} else {
null
}
@ -439,7 +437,7 @@ internal class CryptoManager @Inject constructor(
// (for now at least. Maybe we should alert the user somehow?)
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")
return false
}
@ -535,7 +533,7 @@ internal class CryptoManager @Inject constructor(
eventType: String,
roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>) {
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
if (!isStarted()) {
Timber.v("## encryptEventContent() : wait after e2e init")
internalStart(false)
@ -601,7 +599,7 @@ internal class CryptoManager @Inject constructor(
* @param callback the callback to return data or null
*/
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
GlobalScope.launch(EmptyCoroutineContext) {
GlobalScope.launch {
val result = withContext(coroutineDispatchers.crypto) {
internalDecryptEvent(event, timeline)
}
@ -649,7 +647,7 @@ internal class CryptoManager @Inject constructor(
* @param event the event
*/
fun onToDeviceEvent(event: Event) {
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
onRoomKeyEvent(event)
@ -671,7 +669,7 @@ internal class CryptoManager @Inject constructor(
*/
private fun onRoomKeyEvent(event: Event) {
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")
return
}
@ -689,7 +687,7 @@ internal class CryptoManager @Inject constructor(
* @param event the encryption event.
*/
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
val params = LoadRoomMembersTask.Params(roomId)
loadRoomMembersTask
.execute(params)
@ -738,7 +736,7 @@ internal class CryptoManager @Inject constructor(
val membership = roomMember?.membership
if (membership == Membership.JOIN) {
// make sure we are tracking the deviceList for this user.
deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
deviceListManager.startTrackingDeviceList(listOf(userId))
} else if (membership == Membership.INVITE
&& shouldEncryptForInvitedMembers(roomId)
&& 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.
// They therefore will not send device updates if a user logs in whilst
// 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
*/
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 callback the exported keys
*/
private fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) {
GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
Try {
val iterationCount = Math.max(0, anIterationCount)
private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray {
return withContext(coroutineDispatchers.crypto) {
val iterationCount = 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) {
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)
MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
}
}
@ -879,7 +867,7 @@ internal class CryptoManager @Inject constructor(
*/
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
// force the refresh to ensure that the devices list is up-to-date
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
deviceListManager
.downloadKeys(userIds, true)
.fold(
@ -944,7 +932,7 @@ internal class CryptoManager @Inject constructor(
val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList()
if (add) {
if (!roomIds.contains(roomId)) {
if (roomId !in roomIds) {
roomIds.add(roomId)
}
} else {
@ -1033,8 +1021,7 @@ internal class CryptoManager @Inject constructor(
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
val userIds = devicesInRoom.userIds
for (userId in userIds) {
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
deviceIds?.forEach { deviceId ->
devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId ->
devicesInRoom.getObject(userId, deviceId)
?.takeIf { it.isUnknown }
?.let {
@ -1047,7 +1034,7 @@ internal class CryptoManager @Inject constructor(
}
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
deviceListManager
.downloadKeys(userIds, forceDownload)
.foldToCallback(callback)

View File

@ -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.algorithms.IMXDecrypting
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.event.EncryptedEventContent
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.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.*
import kotlin.collections.HashMap
internal class MXMegolmDecryption(private val credentials: Credentials,
@ -312,7 +310,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
return
}
val userId = request.userId ?: return
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
deviceListManager
.downloadKeys(listOf(userId), false)
.flatMap {
@ -321,8 +319,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
if (deviceInfo == null) {
throw RuntimeException()
} else {
val devicesByUser = HashMap<String, List<MXDeviceInfo>>()
devicesByUser[userId] = ArrayList(Arrays.asList(deviceInfo))
val devicesByUser = mapOf(userId to listOf(deviceInfo))
ensureOlmSessionsForDevicesAction
.handle(devicesByUser)
.flatMap {
@ -336,8 +333,7 @@ internal class MXMegolmDecryption(private val credentials: Credentials,
Timber.v("## shareKeysWithDevice() : sharing keys for session" +
" ${body?.senderKey}|${body?.sessionId} with device $userId:$deviceId")
val payloadJson = HashMap<String, Any>()
payloadJson["type"] = EventType.FORWARDED_ROOM_KEY
val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
olmDevice.getInboundGroupSession(body?.sessionId, body?.senderKey, body?.roomId)
.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>()
sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId")

View File

@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto.model.event
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
/**
* Class representing an encrypted event content
@ -52,5 +53,8 @@ data class EncryptedEventContent(
* The 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
)

View File

@ -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.configureWith
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.*
@ -71,7 +71,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
// Event received from the sync
fun onToDeviceEvent(event: Event) {
CoroutineScope(coroutineDispatchers.crypto).launch {
GlobalScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> {
onStartRequestReceived(event)

View File

@ -18,7 +18,8 @@ package im.vector.matrix.android.internal.network
internal object NetworkConstants {
const val URI_API_PREFIX_PATH = "_matrix/client/"
const val URI_API_PREFIX_PATH_R0 = "_matrix/client/r0/"
private const val URI_API_PREFIX_PATH = "_matrix/client"
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
}
}

View File

@ -94,19 +94,21 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
}
override fun requireBackgroundSync() {
SyncWorker.requireBackgroundSync(context, sessionParams.credentials.userId)
SyncWorker.requireBackgroundSync(context, myUserId)
}
override fun startAutomaticBackgroundSync(repeatDelay: Long) {
SyncWorker.automaticallyBackgroundSync(context, sessionParams.credentials.userId, 0, repeatDelay)
SyncWorker.automaticallyBackgroundSync(context, myUserId, 0, repeatDelay)
}
override fun stopAnyBackgroundSync() {
SyncWorker.stopAnyBackgroundSync(context)
}
override fun startSync() {
override fun startSync(fromForeground : Boolean) {
Timber.i("Starting sync thread")
assert(isOpen)
syncThread.setInitialForeground(fromForeground)
if (!syncThread.isAlive) {
syncThread.start()
} else {

View File

@ -17,9 +17,13 @@ package im.vector.matrix.android.internal.session.room
import arrow.core.Try
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.room.model.message.MessageContent
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.EventMapper
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.
*/
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
private val SHOULD_HANDLE_SERVER_AGREGGATION = false
@ -86,14 +92,43 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(private
}
}
EventAnnotationsSummaryEntity.where(realm, event.eventId ?: "").findFirst()?.let {
TimelineEventEntity.where(realm,eventId = event.eventId ?: "").findFirst()?.let { tet ->
EventAnnotationsSummaryEntity.where(realm, event.eventId
?: "").findFirst()?.let {
TimelineEventEntity.where(realm, eventId = event.eventId
?: "").findFirst()?.let { tet ->
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 -> {
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
?: 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 targetEventId = content.relatesTo?.eventId ?: return
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
val newContent = content.newContent ?: return
//ok, this is a replace
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()

View File

@ -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.roomdirectory.PublicRoomsParams
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.session.room.membership.RoomMembersResponse
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.timeline.EventContextResponse
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
@ -195,6 +196,20 @@ internal interface RoomAPI {
@Body content: Content?
): 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.
*

View File

@ -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.SetReadMarkersTask
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.UpdateQuickReactionTask
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.state.DefaultStateService
@ -56,7 +56,7 @@ internal class RoomFactory @Inject constructor(private val context: Context,
private val setReadMarkersTask: SetReadMarkersTask,
private val cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val updateQuickReactionTask: UpdateQuickReactionTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val joinRoomTask: JoinRoomTask,
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 readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask, credentials)
val relationService = DefaultRelationService(context,
credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, monarchy, taskExecutor)
credentials, roomId, eventFactory, cryptoService, findReactionEventForUndoTask, fetchEditHistoryTask, monarchy, taskExecutor)
return DefaultRoom(
roomId,

View File

@ -142,4 +142,7 @@ internal abstract class RoomModule {
@Binds
abstract fun bindFileService(fileService: DefaultFileService): FileService
@Binds
abstract fun bindFetchEditHistoryTask(editHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask
}

View File

@ -90,7 +90,9 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
val inviteMeEvent = roomMembers.queryRoomMemberEvent(credentials.userId).findFirst()
val inviterId = inviteMeEvent?.sender
name = if (inviterId != null) {
val inviterMemberEvent = loadedMembers.where().equalTo(EventEntityFields.STATE_KEY, inviterId).findFirst()
val inviterMemberEvent = loadedMembers.where()
.equalTo(EventEntityFields.STATE_KEY, inviterId)
.findFirst()
inviterMemberEvent?.toRoomMember()?.displayName
} else {
context.getString(R.string.room_displayname_room_invite)
@ -128,7 +130,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
return if (isUnique) {
roomMember.displayName
} else {
"${roomMember.displayName} ( ${eventEntity.stateKey} )"
"${roomMember.displayName} (${eventEntity.stateKey})"
}
}

View File

@ -61,13 +61,13 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
}
val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId
?: "").findFirst()
?: return
?: "").findFirst()
?: return
val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT
Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho")
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
?: return
?: return
val allowedKeys = computeAllowedKeys(eventToPrune.type)
if (allowedKeys.isNotEmpty()) {
@ -75,10 +75,11 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
eventToPrune.content = ContentMapper.map(prunedContent)
} else {
when (eventToPrune.type) {
EventType.ENCRYPTED,
EventType.MESSAGE -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}")
val unsignedData = EventMapper.map(eventToPrune).unsignedData
?: UnsignedData(null, null)
?: UnsignedData(null, null)
//was this event a m.replace
// 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)
eventToPrune.content = ContentMapper.map(emptyMap())
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
eventToPrune.decryptionResultJson = null
eventToPrune.decryptionErrorCode = null
}
// EventType.REACTION -> {
@ -112,14 +115,14 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
EventType.STATE_ROOM_CREATE -> listOf("creator")
EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule")
EventType.STATE_ROOM_POWER_LEVELS -> listOf("users",
"users_default",
"events",
"events_default",
"state_default",
"ban",
"kick",
"redact",
"invite")
"users_default",
"events",
"events_default",
"state_default",
"ban",
"kick",
"redact",
"invite")
EventType.STATE_ROOM_ALIASES -> listOf("aliases")
EventType.STATE_CANONICAL_ALIAS -> listOf("alias")
EventType.FEEDBACK -> listOf("type", "target_event_id")

View File

@ -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.events.model.Event
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.timeline.TimelineEvent
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 cryptoService: CryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor)
: RelationService {
@ -125,10 +127,50 @@ internal class DefaultRelationService @Inject constructor(private val context: C
.also {
saveLocalEcho(it)
}
val workRequest = createSendEventWork(event)
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest)
return CancelableWork(context, workRequest.id)
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 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? {
@ -169,7 +211,8 @@ internal class DefaultRelationService @Inject constructor(private val context: C
EventAnnotationsSummaryEntity.where(realm, eventId)
}
return Transformations.map(liveEntity) { realmResults ->
realmResults.firstOrNull()?.asDomain() ?: EventAnnotationsSummary(eventId, emptyList(), null)
realmResults.firstOrNull()?.asDomain()
?: EventAnnotationsSummary(eventId, emptyList(), null)
}
}

View File

@ -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
}
}
}

View File

@ -13,9 +13,16 @@
* See the License for the specific language governing permissions and
* 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
class VisibleRoomStore : RxStore<String>()
@JsonClass(generateAdapter = true)
internal data class RelationsResponse(
@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?
)

View File

@ -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 {
return when (attachment.type) {
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 userId = eventReplied.root.senderId ?: return null
val userLink = PermalinkFactory.createPermalink(userId) ?: return null
// <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>
// This is where the reply goes.
val body = bodyForReply(eventReplied.getLastMessageContent())
val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.root.getClearContent().toModel())
val replyFormatted = REPLY_PATTERN.format(
permalink,
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
//
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 replyFallback = StringBuffer("><$userId>")
val replyFallback = StringBuffer("><$originalSenderId>")
lines.forEachIndexed { index, s ->
if (index == 0) {
replyFallback.append(" $s")
@ -269,23 +314,16 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
replyFallback.append("\n>$s")
}
}
replyFallback.append("\n\n").append(replyText)
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)
replyFallback.append("\n\n").append(newBodyText)
return replyFallback.toString()
}
/**
* 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) {
MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_TEXT,
@ -296,7 +334,7 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
formattedText = content.formattedBody
}
}
val isReply = content.relatesTo?.inReplyTo?.eventId != null
val isReply = content.isReply() || originalContent.isReply()
return if (isReply)
TextContent(content.body, formattedText).removeInReplyFallbacks()
else
@ -353,7 +391,16 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
companion object {
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"""
fun isLocalEchoId(eventId: String): Boolean = eventId.startsWith(LOCAL_ID_PREFIX)

View File

@ -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.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
@ -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
}

View File

@ -37,7 +37,6 @@ import javax.inject.Inject
private const val RETRY_WAIT_TIME_MS = 10_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,
private val networkConnectivityChecker: NetworkConnectivityChecker,
@ -54,10 +53,15 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
updateStateTo(SyncState.IDLE)
}
fun setInitialForeground(initialForeground: Boolean) {
val newState = if (initialForeground) SyncState.IDLE else SyncState.PAUSED
updateStateTo(newState)
}
fun restart() = synchronized(lock) {
if (state is SyncState.PAUSED) {
Timber.v("Resume sync...")
updateStateTo(SyncState.RUNNING(catchingUp = true))
updateStateTo(SyncState.RUNNING(afterPause = true))
lock.notify()
}
}
@ -84,7 +88,6 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
Timber.v("Start syncing...")
networkConnectivityChecker.register(this)
backgroundDetectionObserver.register(this)
updateStateTo(SyncState.RUNNING(catchingUp = true))
while (state != SyncState.KILLING) {
if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) {
@ -93,7 +96,10 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
lock.wait()
}
} 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 params = SyncTask.Params(DEFAULT_LONG_POOL_TIMEOUT)
cancelableTask = syncTask.configureWith(params)
@ -133,11 +139,9 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
latch.await()
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")
}
}
@ -148,6 +152,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
}
private fun updateStateTo(newState: SyncState) {
Timber.v("Update state to $newState")
state = newState
liveState.postValue(newState)
}

View File

@ -119,7 +119,7 @@
<string name="verification_emoji_robot">Robota</string>
<string name="verification_emoji_hat">Txanoa</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_thumbsup">Ederto</string>
<string name="verification_emoji_umbrella">Aterkia</string>

View File

@ -2,4 +2,5 @@
<resources>
<string name="summary_message">%1$s: %2$s</string>
<string name="notice_room_invite_no_invitee">%s\'의 초대</string>
<string name="verification_emoji_headphone">헤드폰</string>
</resources>

View File

@ -228,4 +228,14 @@
<!-- All translations should be the same across all Riot clients, please use the same translation than RiotWeb -->
<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>

View File

@ -1,10 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<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>

View File

@ -65,8 +65,8 @@ android {
multiDexEnabled true
// For release, use generateVersionCodeFromVersionName()
versionCode generateVersionCodeFromTimestamp()
// versionCode generateVersionCodeFromVersionName()
// versionCode generateVersionCodeFromTimestamp()
versionCode generateVersionCodeFromVersionName()
versionName "${versionMajor}.${versionMinor}.${versionPatch}"
@ -187,8 +187,9 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.0'
// TODO RxBindings3 exists
implementation 'com.jakewharton.rxbinding2:rxbinding:2.2.0'
// RXBinding
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")
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
@ -254,6 +255,8 @@ dependencies {
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
implementation 'diff_match_patch:diff_match_patch:current'
// TESTS
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'

View File

@ -199,7 +199,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
if (eventType == null) {
//Just add a generic unknown event
val simpleNotifiableEvent = SimpleNotifiableEvent(
session.sessionParams.credentials.userId,
session.myUserId,
eventId,
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),
@ -238,7 +238,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
}
notifiableEvent.isPushGatewayEvent = true
notifiableEvent.matrixID = session.sessionParams.credentials.userId
notifiableEvent.matrixID = session.myUserId
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
notificationDrawerManager.refreshNotificationDrawer()
}

View File

@ -58,8 +58,10 @@
<activity
android:name=".features.reactions.EmojiReactionPickerActivity"
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.roompreview.RoomPreviewActivity" />
<activity android:name=".features.home.room.filtered.FilteredRoomsActivity" />
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
<activity android:name=".features.debug.DebugMenuActivity" />

View File

@ -344,6 +344,13 @@ SOFTWARE.
<br/>
Copyright (c) 2018, Jaisel Rahman
</li>
<li>
<b>diff-match-patch</b>
<br/>
Copyright 2018 The diff-match-patch Authors. https://github.com/google/diff-match-patch
</li>
</ul>
<pre>
Apache License

View File

@ -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.KeysBackupSetupStep3Fragment
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.room.detail.RoomDetailFragment
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment
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.detail.timeline.action.*
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.invite.VectorInviteView
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.roomdirectory.PublicRoomsFragment
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.picker.RoomDirectoryPickerFragment
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
@ -93,6 +95,8 @@ interface ScreenComponent {
fun inject(viewReactionBottomSheet: ViewReactionBottomSheet)
fun inject(viewEditHistoryBottomSheet: ViewEditHistoryBottomSheet)
fun inject(messageMenuFragment: MessageMenuFragment)
fun inject(vectorSettingsActivity: VectorSettingsActivity)
@ -131,6 +135,10 @@ interface ScreenComponent {
fun inject(imageMediaViewerActivity: ImageMediaViewerActivity)
fun inject(filteredRoomsActivity: FilteredRoomsActivity)
fun inject(createRoomActivity: CreateRoomActivity)
fun inject(vectorInviteView: VectorInviteView)
fun inject(videoMediaViewerActivity: VideoMediaViewerActivity)

View File

@ -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.setup.KeysBackupSetupSharedViewModel
import im.vector.riotx.features.crypto.verification.SasVerificationViewModel
import im.vector.riotx.features.home.HomeActivityViewModel
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.*
import im.vector.riotx.features.home.group.GroupListViewModel
import im.vector.riotx.features.home.group.GroupListViewModel_AssistedFactory
import im.vector.riotx.features.home.room.detail.RoomDetailViewModel
@ -59,11 +55,17 @@ import im.vector.riotx.features.workers.signout.SignOutViewModel
@Module
interface ViewModelModule {
/**
* ViewModels with @IntoMap will be injected by this factory
*/
@Binds
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
@IntoMap
@ViewModelKey(SignOutViewModel::class)
@ -114,6 +116,10 @@ interface ViewModelModule {
@ViewModelKey(ConfigurationViewModel::class)
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
fun bindHomeActivityViewModelFactory(factory: HomeActivityViewModel_AssistedFactory): HomeActivityViewModel.Factory
@ -156,6 +162,9 @@ interface ViewModelModule {
@Binds
fun bindViewReactionViewModelFactory(factory: ViewReactionViewModel_AssistedFactory): ViewReactionViewModel.Factory
@Binds
fun bindViewEditHistoryViewModelFactory(factory: ViewEditHistoryViewModel_AssistedFactory): ViewEditHistoryViewModel.Factory
@Binds
fun bindCreateRoomViewModelFactory(factory: CreateRoomViewModel_AssistedFactory): CreateRoomViewModel.Factory

View File

@ -23,8 +23,8 @@ fun AppCompatActivity.addFragment(fragment: Fragment, frameId: Int) {
supportFragmentManager.inTransaction { add(frameId, fragment) }
}
fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int) {
supportFragmentManager.inTransaction { replace(frameId, fragment) }
fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int, tag: String? = null) {
supportFragmentManager.inTransaction { replace(frameId, fragment, tag) }
}
fun AppCompatActivity.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {

View File

@ -16,14 +16,20 @@
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.sync.FilterService
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import timber.log.Timber
fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener) {
open()
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()
pushRuleTriggerListener.startWithSession(this)
fetchPushRules()

View File

@ -21,5 +21,5 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
fun TimelineEvent.canReact(): Boolean {
// 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()
}

View File

@ -58,10 +58,10 @@ open class UserAvatarPreference : Preference {
open fun refreshAvatar() {
val session = mSession ?: return
val view = mAvatarView ?: return
session.getUser(session.sessionParams.credentials.userId)?.let {
session.getUser(session.myUserId)?.let {
avatarRenderer.render(it, view)
} ?: run {
avatarRenderer.render(null, session.sessionParams.credentials.userId, null, view)
avatarRenderer.render(null, session.myUserId, null, view)
}
}

View File

@ -51,7 +51,7 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
var title: String? = null
@EpoxyAttribute
var description: String? = null
var description: CharSequence? = null
@EpoxyAttribute
var style: STYLE = STYLE.NORMAL_TEXT

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -28,6 +28,7 @@ import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import im.vector.riotx.R
@ -81,11 +82,11 @@ fun requestDisablingBatteryOptimization(activity: Activity, fragment: Fragment?,
* @param context the context
* @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
clipboard.primaryClip = ClipData.newPlainText("", text)
if (showToast) {
context.toast(R.string.copied_to_clipboard)
context.toast(toastMessage)
}
}

View File

@ -21,8 +21,8 @@ import androidx.lifecycle.ViewModel
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.StepProgressListener
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.model.ImportRoomKeysResult
import im.vector.riotx.R
import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.core.ui.views.KeysBackupBanner
@ -57,7 +57,7 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor() : ViewModel() {
keysBackup.restoreKeysWithRecoveryKey(keysVersionResult,
recoveryKey,
null,
session.sessionParams.credentials.userId,
session.myUserId,
object : StepProgressListener {
override fun onStepProgress(step: StepProgressListener.Step) {
when (step) {

View File

@ -21,8 +21,8 @@ import androidx.lifecycle.ViewModel
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.StepProgressListener
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.model.ImportRoomKeysResult
import im.vector.riotx.R
import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.core.ui.views.KeysBackupBanner
@ -58,7 +58,7 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor() : ViewModel
keysBackup.restoreKeyBackupWithPassword(keysVersionResult,
passphrase.value!!,
null,
sharedViewModel.session.sessionParams.credentials.userId,
sharedViewModel.session.myUserId,
object : StepProgressListener {
override fun onStepProgress(step: StepProgressListener.Step) {
when (step) {

View File

@ -41,6 +41,7 @@ import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.pushers.PushersManager
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.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.workers.signout.SignOutViewModel
@ -64,6 +65,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory
@Inject lateinit var homeNavigator: HomeNavigator
@Inject lateinit var navigator: Navigator
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var pushManager: PushersManager
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@ -192,6 +194,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
bugReporter.openBugReportScreen(this, false)
return true
}
R.id.menu_home_filter -> {
navigator.openRoomsFiltering(this)
return true
}
}
return true

View File

@ -209,7 +209,7 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
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
}
syncProgressBarWrap.visibility = syncProgressBar.visibility

View File

@ -52,7 +52,7 @@ class HomeDrawerFragment : VectorBaseFragment() {
replaceChildFragment(groupListFragment, R.id.homeDrawerGroupListContainer)
}
session.observeUser(session.sessionParams.credentials.userId).observeK(this) { user ->
session.observeUser(session.myUserId).observeK(this) { user ->
if (user != null) {
avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView)
homeDrawerUsernameView.text = user.displayName

View File

@ -18,10 +18,6 @@ package im.vector.riotx.features.home
import im.vector.matrix.android.api.session.room.model.RoomSummary
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.Singleton

View File

@ -93,7 +93,7 @@ class GroupListViewModel @AssistedInject constructor(@Assisted initialState: Gro
.rx()
.liveGroupSummaries()
.map {
val myUser = session.getUser(session.sessionParams.credentials.userId)
val myUser = session.getUser(session.myUserId)
val allCommunityGroup = GroupSummary(
groupId = ALL_COMMUNITIES_GROUP_ID,
displayName = stringProvider.getString(R.string.group_all_communities),

View File

@ -32,7 +32,6 @@ sealed class RoomDetailActions {
data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions()
data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions()
data class UpdateQuickReactAction(val targetEventId: String, val selectedReaction: String, val add: Boolean) : RoomDetailActions()
data class ShowEditHistoryAction(val event: String, val editAggregatedSummary: EditAggregatedSummary) : RoomDetailActions()
data class NavigateToEvent(val eventId: String, val position: Int?) : RoomDetailActions()
data class DownloadFile(val eventId: String, val messageFileContent: MessageFileContent) : RoomDetailActions()
object AcceptInvite : RoomDetailActions()

View File

@ -35,7 +35,7 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
super.onCreate(savedInstanceState)
if (isFirstCreation()) {
val roomDetailArgs: RoomDetailArgs = intent?.extras?.getParcelable(EXTRA_ROOM_DETAIL_ARGS)
?: return
?: return
val roomDetailFragment = RoomDetailFragment.newInstance(roomDetailArgs)
replaceFragment(roomDetailFragment, R.id.roomDetailContainer)
}

View File

@ -37,9 +37,11 @@ import androidx.annotation.DrawableRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.args
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.Membership
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.getLastMessageContent
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R
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.TextComposerViewState
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.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.action.*
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.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView
@ -261,7 +262,7 @@ class RoomDetailFragment :
composerLayout.composerRelatedMessageContent.text = formattedBody
?: nonFormattedBody
composerLayout.composerEditText.setText(if (useText) nonFormattedBody else "")
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
avatarRenderer.render(event.senderAvatar, event.root.senderId
@ -269,8 +270,10 @@ class RoomDetailFragment :
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
composerLayout.expand {
//need to do it here also when not using quick reply
focusComposerAndShowKeyboard()
}
focusComposerAndShowKeyboard()
}
override fun onResume() {
@ -326,6 +329,32 @@ class RoomDetailFragment :
})
recyclerView.setController(timelineEventController)
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() {
@ -489,7 +518,7 @@ class RoomDetailFragment :
timelineEventController.setTimeline(state.timeline, state.eventId)
inviteView.visibility = View.GONE
val uid = session.sessionParams.credentials.userId
val uid = session.myUserId
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)
@ -575,7 +604,7 @@ class RoomDetailFragment :
override fun onUrlLongClicked(url: String): Boolean {
// Copy the url to the clipboard
copyToClipboard(requireContext(), url)
copyToClipboard(requireContext(), url, true, R.string.link_copied_to_clipboard)
return true
}
@ -666,10 +695,8 @@ class RoomDetailFragment :
}
override fun onEditedDecorationClicked(informationData: MessageInformationData, editAggregatedSummary: EditAggregatedSummary?) {
editAggregatedSummary?.also {
roomDetailViewModel.process(RoomDetailActions.ShowEditHistoryAction(informationData.eventId, it))
}
ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_EDITS")
}
// AutocompleteUserPresenter.Callback
@ -785,7 +812,7 @@ class RoomDetailFragment :
if (null != text) {
// var vibrate = false
val myDisplayName = session.getUser(session.sessionParams.credentials.userId)?.displayName
val myDisplayName = session.getUser(session.myUserId)?.displayName
if (TextUtils.equals(myDisplayName, text)) {
// current user
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
@ -833,10 +860,12 @@ class RoomDetailFragment :
// VectorInviteView.Callback
override fun onAcceptInvite() {
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
roomDetailViewModel.process(RoomDetailActions.AcceptInvite)
}
override fun onRejectInvite() {
notificationDrawerManager.clearMemberShipNotificationForRoom(roomDetailArgs.roomId)
roomDetailViewModel.process(RoomDetailActions.RejectInvite)
}
}

View File

@ -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.timeline.TimelineEvent
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.riotx.R
import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.resources.UserPreferencesProvider
@ -52,8 +52,6 @@ import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
@ -114,7 +112,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailActions.RedactAction -> handleRedactEvent(action)
is RoomDetailActions.UndoReaction -> handleUndoReact(action)
is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailActions.ShowEditHistoryAction -> handleShowEditHistoryReaction(action)
is RoomDetailActions.EnterEditMode -> handleEditAction(action)
is RoomDetailActions.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailActions.EnterReplyMode -> handleReplyAction(action)
@ -230,16 +227,27 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
}
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) {
room.editTextMessage(state.sendMode.timelineEvent.root.eventId
?: "", messageContent?.type ?: MessageType.MSGTYPE_TEXT, action.text, action.autoMarkdown)
//is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
?: 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 {
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 {
copy(
@ -309,22 +317,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
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) {
_sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled))
@ -364,7 +356,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
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) {
room.sendReaction(action.selectedReaction, action.targetEventId)
} else {
room.undoReaction(action.selectedReaction, action.targetEventId, session.sessionParams.credentials.userId)
room.undoReaction(action.selectedReaction, action.targetEventId, session.myUserId)
}
}

View File

@ -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
)
}
}

View File

@ -57,6 +57,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
var currentConstraintSetId: Int = -1
private val animationDuration = 100L
init {
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
if (animate) {
val transition = AutoTransition()
// transition.duration = 5000
transition.duration = animationDuration
transition.addListener(object : Transition.TransitionListener {
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
if (animate) {
val transition = AutoTransition()
// transition.duration = 5000
transition.duration = animationDuration
transition.addListener(object : Transition.TransitionListener {
override fun onTransitionEnd(transition: Transition) {

View File

@ -18,7 +18,6 @@ package im.vector.riotx.features.home.room.detail.timeline
import android.os.Handler
import android.os.Looper
import android.util.LongSparseArray
import android.view.View
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
@ -84,7 +83,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
}
private val collapsedEventIds = linkedSetOf<Long>()
private val mergeItemCollapseStates = HashMap<Long,Boolean>()
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
private val modelCache = arrayListOf<CacheItemData?>()
private var currentSnapshot: List<TimelineEvent> = emptyList()
@ -178,16 +177,19 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
}
override fun buildModels() {
LoadingItem_()
val loaderAdded = LoadingItem_()
.id("forward_loading_item")
.addWhen(Timeline.Direction.FORWARDS)
val timelineModels = getModels()
add(timelineModels)
LoadingItem_()
.id("backward_loading_item")
.addWhen(Timeline.Direction.BACKWARDS)
// Avoid displaying two loaders if there is no elements between them
if (!loaderAdded || timelineModels.isNotEmpty()) {
LoadingItem_()
.id("backward_loading_item")
.addWhen(Timeline.Direction.BACKWARDS)
}
}
// 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
addIf(shouldAdd, this@TimelineEventController)
return shouldAdd
}
fun searchPositionOfEvent(eventId: String): Int? {

View File

@ -126,51 +126,55 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
}
//TODO is downloading attachement?
if (event.canReact()) {
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 (!event.root.isRedacted()) {
if (canReply(event, messageContent)) {
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId))
}
if (canEdit(event, session.sessionParams.credentials.userId)) {
this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, eventId))
}
if (canRedact(event, session.sessionParams.credentials.userId)) {
this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, eventId))
}
if (canQuote(event, messageContent)) {
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId))
}
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))
)
if (canReply(event, messageContent)) {
this.add(SimpleAction(ACTION_REPLY, R.string.reply, R.drawable.ic_reply, eventId))
}
if (canEdit(event, session.myUserId)) {
this.add(SimpleAction(ACTION_EDIT, R.string.edit, R.drawable.ic_edit, eventId))
}
if (canRedact(event, session.myUserId)) {
this.add(SimpleAction(ACTION_DELETE, R.string.delete, R.drawable.ic_delete, 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 (event.canReact()) {
this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, eventId))
}
if (canQuote(event, messageContent)) {
this.add(SimpleAction(ACTION_QUOTE, R.string.quote, R.drawable.ic_quote, eventId))
}
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()))
@ -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))
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
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
if (event.root.getClearType() != EventType.MESSAGE) return false
//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 && (
messageContent?.type == MessageType.MSGTYPE_TEXT
|| messageContent?.type == MessageType.MSGTYPE_EMOTE

View File

@ -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 }
}
}
}

View File

@ -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)
}
}

View File

@ -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
)
}
}
})
}
}

View File

@ -21,7 +21,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import butterknife.BindView
import butterknife.ButterKnife
@ -33,7 +32,7 @@ import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
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
/**
@ -49,14 +48,16 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
@BindView(R.id.bottom_sheet_display_reactions_list)
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) {
screenComponent.inject(this)
}
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)
return view
}
@ -67,16 +68,11 @@ class ViewReactionBottomSheet : VectorBaseBottomSheetDialogFragment() {
val dividerItemDecoration = DividerItemDecoration(epoxyRecyclerView.context,
LinearLayout.VERTICAL)
epoxyRecyclerView.addItemDecoration(dividerItemDecoration)
bottomSheetTitle.text = context?.getString(R.string.reactions)
}
override fun invalidate() = withState(viewModel) {
if (it.mapReactionKeyToMemberList() == null) {
bottomSheetViewReactionSpinner.isVisible = true
bottomSheetViewReactionSpinner.animate()
} else {
bottomSheetViewReactionSpinner.isVisible = false
}
epoxyController.setData(it)
}

View File

@ -16,24 +16,47 @@
package im.vector.riotx.features.home.room.detail.timeline.action
import android.content.Context
import android.graphics.Typeface
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
*/
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) {
val map = state.mapReactionKeyToMemberList() ?: return
map.forEach {
reactionInfoSimpleItem {
id(it.eventId)
emojiTypeFace(emojiCompatTypeface)
timeStamp(it.timestamp)
reactionKey(it.reactionKey)
authorDisplayName(it.authorName ?: it.authorId)
when (state.mapReactionKeyToMemberList) {
is Incomplete -> {
genericLoaderItem {
id("Spinner")
}
}
is Fail -> {
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)
}
}
}
}
}
}

View File

@ -68,6 +68,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
return MessageTextItem_()
.message(spannableStr)
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)

View File

@ -27,12 +27,14 @@ import dagger.Lazy
import im.vector.matrix.android.api.permalinks.MatrixLinkify
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.toModel
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.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@ -83,7 +85,9 @@ class MessageItemFactory @Inject constructor(
?: //Malformed content, we should echo something on screen
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
return BlankItem_()
}
@ -117,6 +121,7 @@ class MessageItemFactory @Inject constructor(
callback: TimelineEventController.Callback?): MessageFileItem? {
return MessageFileItem_()
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
@ -144,6 +149,7 @@ class MessageItemFactory @Inject constructor(
callback: TimelineEventController.Callback?): MessageFileItem? {
return MessageFileItem_()
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
@ -195,6 +201,7 @@ class MessageItemFactory @Inject constructor(
)
return MessageImageVideoItem_()
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.playable(messageContent.info?.mimeType == "image/gif")
@ -226,7 +233,8 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
url = messageContent.videoInfo?.thumbnailFile?.url
?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height,
maxHeight = maxHeight,
@ -246,6 +254,7 @@ class MessageItemFactory @Inject constructor(
.imageContentRenderer(imageContentRenderer)
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.playable(true)
.informationData(informationData)
.highlighted(highlight)
@ -288,6 +297,7 @@ class MessageItemFactory @Inject constructor(
}
.avatarRenderer(avatarRenderer)
.informationData(informationData)
.colorProvider(colorProvider)
.highlighted(highlight)
.avatarCallback(callback)
.urlClickCallback(callback)
@ -353,6 +363,7 @@ class MessageItemFactory @Inject constructor(
return MessageTextItem_()
.avatarRenderer(avatarRenderer)
.message(message)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
@ -393,6 +404,7 @@ class MessageItemFactory @Inject constructor(
}
}
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight)
.avatarCallback(callback)
@ -414,9 +426,18 @@ class MessageItemFactory @Inject constructor(
callback: TimelineEventController.Callback?): RedactedMessageItem? {
return RedactedMessageItem_()
.avatarRenderer(avatarRenderer)
.colorProvider(colorProvider)
.informationData(informationData)
.highlighted(highlight)
.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 {

View File

@ -23,12 +23,16 @@ import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.view.isVisible
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.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.ui.getMessageTextColor
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>()
@ -38,7 +42,7 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
activeSessionHolder.getActiveSession().also { session ->
val uploadStateTracker = session.contentUploadProgressTracker()
val updateListener = ContentMediaProgressUpdater(progressLayout, mediaData)
val updateListener = ContentMediaProgressUpdater(progressLayout, mediaData, colorProvider)
updateListeners[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 val mediaData: ImageContentRenderer.Data) : ContentUploadStateTracker.UpdateListener {
private val mediaData: ImageContentRenderer.Data,
private val colorProvider: ColorProvider) : ContentUploadStateTracker.UpdateListener {
override fun onUpdate(state: ContentUploadStateTracker.State) {
when (state) {
@ -79,6 +84,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
progressBar?.isIndeterminate = true
progressBar?.progress = 0
progressTextView?.text = progressLayout.context.getString(R.string.send_file_step_idle)
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.UNSENT))
} else {
progressLayout.isVisible = false
}
@ -106,6 +112,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
progressBar?.isIndeterminate = true
progressTextView?.text = progressLayout.context.getString(resId)
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.ENCRYPTING))
}
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,
Formatter.formatShortFileSize(progressLayout.context, current),
Formatter.formatShortFileSize(progressLayout.context, total))
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.SENDING))
}
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 progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
progressBar?.isVisible = false
// TODO Red text
progressTextView?.text = state.throwable.localizedMessage
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.UNDELIVERED))
}
private fun handleSuccess(state: ContentUploadStateTracker.State.Success) {

View File

@ -23,24 +23,32 @@ import android.view.ViewGroup
import android.view.ViewStub
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.IdRes
import androidx.constraintlayout.helper.widget.Flow
import androidx.core.view.children
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
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.DimensionUtils.dpToPx
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.reactions.widget.ReactionButton
import im.vector.riotx.features.ui.getMessageTextColor
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
var longClickListener: View.OnLongClickListener? = null
@ -153,13 +161,12 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
return true
}
protected fun View.renderSendState() {
isClickable = informationData.sendState.isSent()
alpha = if (informationData.sendState.isSent()) 1f else 0.5f
protected fun renderSendState(root: View, textView: TextView?) {
root.isClickable = informationData.sendState.isSent()
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 memberNameView by bind<TextView>(R.id.messageMemberNameView)
val timeView by bind<TextView>(R.id.messageTimeView)

View File

@ -26,6 +26,9 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.platform.CheckableView
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
/**
* Children must override getViewType()
*/
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
var avatarStyle: AvatarStyle = AvatarStyle.SMALL
@ -43,31 +46,18 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
holder.checkableBackground.isChecked = highlighted
}
override fun getViewType(): Int {
return getStubType()
}
abstract fun getStubType(): Int
abstract class BaseHolder : VectorEpoxyHolder() {
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
val leftGuideline by bind<Guideline>(R.id.messageStartGuideline)
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
@IdRes
abstract fun getStubId(): Int
override fun bindView(itemView: View) {
super.bindView(itemView)
inflateStub()
}
private fun inflateStub() {
view.findViewById<ViewStub>(getStubId()).inflate()
view.findViewById<ViewStub>(stubId).inflate()
}
}
companion object {

View File

@ -31,11 +31,9 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
holder.messageView.text = text
}
override fun getStubType(): Int = STUB_ID
class Holder : BaseHolder() {
override fun getStubId(): Int = STUB_ID
override fun getViewType() = STUB_ID
class Holder : BaseHolder(STUB_ID) {
val messageView by bind<TextView>(R.id.stateMessageView)
}

View File

@ -46,7 +46,7 @@ data class MergedHeaderItem(private val isCollapsed: Boolean,
return Holder()
}
override fun getStubType(): Int = STUB_ID
override fun getViewType() = STUB_ID
override fun bind(holder: Holder) {
super.bind(holder)
@ -84,8 +84,7 @@ data class MergedHeaderItem(private val isCollapsed: Boolean,
val avatarUrl: String?
)
class Holder : BaseHolder() {
override fun getStubId(): Int = STUB_ID
class Holder : BaseHolder(STUB_ID) {
val expandView by bind<TextView>(R.id.itemMergedExpandTextView)
val summaryView by bind<TextView>(R.id.itemMergedSummaryTextView)
@ -95,6 +94,6 @@ data class MergedHeaderItem(private val isCollapsed: Boolean,
}
companion object {
private val STUB_ID = R.id.messageContentMergedheaderStub
private const val STUB_ID = R.id.messageContentMergedheaderStub
}
}

View File

@ -25,7 +25,6 @@ import androidx.annotation.DrawableRes
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
@ -36,34 +35,27 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
@DrawableRes
var iconRes: Int = 0
@EpoxyAttribute
override lateinit var informationData: MessageInformationData
@EpoxyAttribute
override lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute
var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.fileLayout.renderSendState()
renderSendState(holder.fileLayout, holder.filenameView)
holder.filenameView.text = filename
holder.fileImageView.setImageResource(iconRes)
holder.filenameView.setOnClickListener(clickListener)
holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG)
}
override fun getStubType(): Int = STUB_ID
class Holder : AbsMessageItem.Holder() {
override fun getStubId(): Int = STUB_ID
override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
val fileImageView by bind<ImageView>(R.id.messageFileImageView)
val filenameView by bind<TextView>(R.id.messageFilenameView)
}
companion object {
private val STUB_ID = R.id.messageContentFileStub
private const val STUB_ID = R.id.messageContentFileStub
}
}

View File

@ -22,7 +22,6 @@ import android.widget.ImageView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.riotx.features.media.ImageContentRenderer
@ -32,10 +31,6 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
@EpoxyAttribute
lateinit var mediaData: ImageContentRenderer.Data
@EpoxyAttribute
override lateinit var informationData: MessageInformationData
@EpoxyAttribute
override lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute
var playable: Boolean = false
@EpoxyAttribute
var clickListener: View.OnClickListener? = null
@ -52,7 +47,8 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
holder.imageView.setOnLongClickListener(longClickListener)
holder.mediaContentView.setOnClickListener(cellClickListener)
holder.mediaContentView.setOnLongClickListener(longClickListener)
holder.imageView.renderSendState()
// The sending state color will be apply to the progress text
renderSendState(holder.imageView, null)
holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
}
@ -61,23 +57,17 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
super.unbind(holder)
}
override fun getStubType(): Int = STUB_ID
class Holder : AbsMessageItem.Holder() {
override fun getStubId(): Int = STUB_ID
override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
val imageView by bind<ImageView>(R.id.messageThumbnailView)
val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia)
}
companion object {
private val STUB_ID = R.id.messageContentMediaStub
private const val STUB_ID = R.id.messageContentMediaStub
}
}

View File

@ -16,6 +16,7 @@
package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.MotionEvent
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.text.PrecomputedTextCompat
import androidx.core.text.toSpannable
@ -24,7 +25,6 @@ import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.utils.containsOnlyEmojis
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.html.PillImageSpan
import kotlinx.coroutines.Dispatchers
@ -39,20 +39,25 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
@EpoxyAttribute
var message: CharSequence? = null
@EpoxyAttribute
override lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute
override lateinit var informationData: MessageInformationData
@EpoxyAttribute
var urlClickCallback: TimelineEventController.UrlClickCallback? = null
// Better link movement methods fixes the issue when
// long pressing to open the context menu on a TextView also triggers an autoLink click.
private val mvmtMethod = BetterLinkMovementMethod.newInstance().also {
it.setOnLinkClickListener { _, url ->
//Return false to let android manage the click on the link, or true if the link is handled by the application
urlClickCallback?.onUrlClicked(url) == true
}
it.setOnLinkLongClickListener { _, url ->
//We need also to fix the case when long click on link will trigger long click on cell
it.setOnLinkLongClickListener { tv, url ->
//Long clicks are handled by parent, return true to block android to do something with url
urlClickCallback?.onUrlLongClicked(url) == true
if (urlClickCallback?.onUrlLongClicked(url) == true) {
tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0))
true
} else {
false
}
}
}
@ -73,7 +78,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
null)
holder.messageView.setTextFuture(textFuture)
holder.messageView.renderSendState()
renderSendState(holder.messageView, holder.messageView)
holder.messageView.setOnClickListener(cellClickListener)
holder.messageView.setOnLongClickListener(longClickListener)
findPillsAndProcess { it.bind(holder.messageView) }
@ -90,12 +95,13 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
}
}
override fun getStubType(): Int = R.id.messageContentTextStub
override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder() {
class Holder : AbsMessageItem.Holder(STUB_ID) {
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
override fun getStubId(): Int = R.id.messageContentTextStub
}
companion object {
private const val STUB_ID = R.id.messageContentTextStub
}
}

View File

@ -57,10 +57,9 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
holder.view.setOnLongClickListener(longClickListener)
}
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 avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
}

View File

@ -16,28 +16,19 @@
package im.vector.riotx.features.home.room.detail.timeline.item
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class RedactedMessageItem : AbsMessageItem<RedactedMessageItem.Holder>() {
@EpoxyAttribute
override lateinit var informationData: MessageInformationData
@EpoxyAttribute
override lateinit var avatarRenderer: AvatarRenderer
override fun getStubType(): Int = STUB_ID
override fun getViewType() = STUB_ID
override fun shouldShowReactionAtBottom() = false
class Holder : AbsMessageItem.Holder() {
override fun getStubId(): Int = STUB_ID
}
class Holder : AbsMessageItem.Holder(STUB_ID)
companion object {
private val STUB_ID = R.id.messageContentRedactedStub
private const val STUB_ID = R.id.messageContentRedactedStub
}
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.filtered
import android.widget.Button
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.features.home.room.list.widget.FabMenuView
@EpoxyModelClass(layout = R.layout.item_room_filter_footer)
abstract class FilteredRoomFooterItem : VectorEpoxyModel<FilteredRoomFooterItem.Holder>() {
@EpoxyAttribute
var listener: FilteredRoomFooterItemListener? = null
@EpoxyAttribute
var currentFilter: String = ""
override fun bind(holder: Holder) {
holder.createRoomButton.setOnClickListener { listener?.createRoom(currentFilter) }
holder.createDirectChat.setOnClickListener { listener?.createDirectChat() }
holder.openRoomDirectory.setOnClickListener { listener?.openRoomDirectory(currentFilter) }
}
class Holder : VectorEpoxyHolder() {
val createRoomButton by bind<Button>(R.id.roomFilterFooterCreateRoom)
val createDirectChat by bind<Button>(R.id.roomFilterFooterCreateDirect)
val openRoomDirectory by bind<Button>(R.id.roomFilterFooterOpenRoomDirectory)
}
interface FilteredRoomFooterItemListener : FabMenuView.Listener {
fun createRoom(initialName: String)
}
}

View File

@ -0,0 +1,78 @@
/*
* 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.filtered
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.widget.SearchView
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.home.room.list.RoomListParams
import kotlinx.android.synthetic.main.activity_filtered_rooms.*
class FilteredRoomsActivity : VectorBaseActivity() {
private lateinit var roomListFragment: RoomListFragment
override fun getLayoutRes(): Int {
return R.layout.activity_filtered_rooms
}
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
configureToolbar(filteredRoomsToolbar)
if (isFirstCreation()) {
roomListFragment = RoomListFragment.newInstance(RoomListParams(RoomListFragment.DisplayMode.FILTERED))
replaceFragment(roomListFragment, R.id.filteredRoomsFragmentContainer, FRAGMENT_TAG)
} else {
roomListFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as RoomListFragment
}
filteredRoomsSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return true
}
override fun onQueryTextChange(newText: String): Boolean {
// TODO Create a viewModel and remove this public fun
roomListFragment.filterRoomsWith(newText)
return true
}
})
// Open the keyboard immediately
filteredRoomsSearchView.requestFocus()
}
companion object {
private const val FRAGMENT_TAG = "RoomListFragment"
fun newIntent(context: Context): Intent {
return Intent(context, FilteredRoomsActivity::class.java)
}
}
}

View File

@ -28,4 +28,6 @@ sealed class RoomListActions {
data class RejectInvitation(val roomSummary: RoomSummary) : RoomListActions()
data class FilterWith(val filter: String) : RoomListActions()
}

View File

@ -27,9 +27,10 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListFragment.Displa
return false
}
return when (displayMode) {
RoomListFragment.DisplayMode.HOME -> roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE
RoomListFragment.DisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
RoomListFragment.DisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
RoomListFragment.DisplayMode.HOME -> roomSummary.notificationCount > 0 || roomSummary.membership == Membership.INVITE
RoomListFragment.DisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
RoomListFragment.DisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
RoomListFragment.DisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN
}
}
}

View File

@ -38,6 +38,7 @@ import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.StateView
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.room.list.widget.FabMenuView
import im.vector.riotx.features.notifications.NotificationDrawerManager
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_list.*
import javax.inject.Inject
@ -53,7 +54,8 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
enum class DisplayMode(@StringRes val titleRes: Int) {
HOME(R.string.bottom_action_home),
PEOPLE(R.string.bottom_action_people_x),
ROOMS(R.string.bottom_action_rooms)
ROOMS(R.string.bottom_action_rooms),
FILTERED(/* Not used */ R.string.bottom_action_rooms)
}
companion object {
@ -68,6 +70,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
@Inject lateinit var roomController: RoomSummaryController
@Inject lateinit var roomListViewModelFactory: RoomListViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
private val roomListViewModel: RoomListViewModel by fragmentViewModel()
override fun getLayoutResId() = R.layout.fragment_room_list
@ -97,9 +100,10 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
private fun setupCreateRoomButton() {
when (roomListParams.displayMode) {
DisplayMode.HOME -> createChatFabMenu.isVisible = true
DisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
else -> createGroupRoomButton.isVisible = true
DisplayMode.HOME -> createChatFabMenu.isVisible = true
DisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
DisplayMode.ROOMS -> createGroupRoomButton.isVisible = true
DisplayMode.FILTERED -> Unit // No button in this mode
}
createChatRoomButton.setOnClickListener {
@ -110,7 +114,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
}
// Hide FAB when list is scrolling
epoxyRecyclerView.addOnScrollListener(
roomListEpoxyRecyclerView.addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
createChatFabMenu.removeCallbacks(showFabRunnable)
@ -122,9 +126,10 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> {
when (roomListParams.displayMode) {
DisplayMode.HOME -> createChatFabMenu.hide()
DisplayMode.PEOPLE -> createChatRoomButton.hide()
else -> createGroupRoomButton.hide()
DisplayMode.HOME -> createChatFabMenu.hide()
DisplayMode.PEOPLE -> createChatRoomButton.hide()
DisplayMode.ROOMS -> createGroupRoomButton.hide()
DisplayMode.FILTERED -> Unit
}
}
}
@ -132,9 +137,15 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
})
}
fun filterRoomsWith(filter: String) {
// Scroll the list to top
roomListEpoxyRecyclerView.scrollToPosition(0)
override fun openRoomDirectory() {
navigator.openRoomDirectory(requireActivity())
roomListViewModel.accept(RoomListActions.FilterWith(filter))
}
override fun openRoomDirectory(initialFilter: String) {
navigator.openRoomDirectory(requireActivity(), initialFilter)
}
override fun createDirectChat() {
@ -144,20 +155,21 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
epoxyRecyclerView.layoutManager = layoutManager
epoxyRecyclerView.itemAnimator = RoomListAnimator()
roomListEpoxyRecyclerView.layoutManager = layoutManager
roomListEpoxyRecyclerView.itemAnimator = RoomListAnimator()
roomController.listener = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController)
stateView.contentView = roomListEpoxyRecyclerView
roomListEpoxyRecyclerView.setController(roomController)
}
private val showFabRunnable = Runnable {
if (isAdded) {
when (roomListParams.displayMode) {
DisplayMode.HOME -> createChatFabMenu.show()
DisplayMode.PEOPLE -> createChatRoomButton.show()
else -> createGroupRoomButton.show()
DisplayMode.HOME -> createChatFabMenu.show()
DisplayMode.PEOPLE -> createChatRoomButton.show()
DisplayMode.ROOMS -> createGroupRoomButton.show()
DisplayMode.FILTERED -> Unit
}
}
}
@ -188,7 +200,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
}
.isNullOrEmpty()
val emptyState = when (roomListParams.displayMode) {
DisplayMode.HOME -> {
DisplayMode.HOME -> {
if (hasNoRoom) {
StateView.State.Empty(
getString(R.string.room_list_catchup_welcome_title),
@ -202,18 +214,21 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
getString(R.string.room_list_catchup_empty_body))
}
}
DisplayMode.PEOPLE ->
DisplayMode.PEOPLE ->
StateView.State.Empty(
getString(R.string.room_list_people_empty_title),
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_chat),
getString(R.string.room_list_people_empty_body)
)
DisplayMode.ROOMS ->
DisplayMode.ROOMS ->
StateView.State.Empty(
getString(R.string.room_list_rooms_empty_title),
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_group),
getString(R.string.room_list_rooms_empty_body)
)
DisplayMode.FILTERED ->
// Always display the content in this mode, because if the footer
StateView.State.Content
}
stateView.state = emptyState
}
@ -245,14 +260,21 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
}
override fun onAcceptRoomInvitation(room: RoomSummary) {
notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId)
roomListViewModel.accept(RoomListActions.AcceptInvitation(room))
}
override fun onRejectRoomInvitation(room: RoomSummary) {
notificationDrawerManager.clearMemberShipNotificationForRoom(room.roomId)
roomListViewModel.accept(RoomListActions.RejectInvitation(room))
}
override fun onToggleRoomCategory(roomCategory: RoomCategory) {
roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory))
}
override fun createRoom(initialName: String) {
navigator.openCreateRoom(requireActivity(), initialName)
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.list
import im.vector.matrix.android.api.session.room.model.RoomSummary
import io.reactivex.functions.Predicate
import javax.inject.Inject
class RoomListNameFilter @Inject constructor() : Predicate<RoomSummary> {
var filter: String = ""
override fun test(roomSummary: RoomSummary): Boolean {
if (filter.isBlank()) {
// No filter
return true
}
return roomSummary.displayName.contains(filter, ignoreCase = true)
}
}

View File

@ -76,6 +76,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
is RoomListActions.ToggleCategory -> handleToggleCategory(action)
is RoomListActions.AcceptInvitation -> handleAcceptInvitation(action)
is RoomListActions.RejectInvitation -> handleRejectInvitation(action)
is RoomListActions.FilterWith -> handleFilter(action)
}
}
@ -89,10 +90,21 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
this.toggle(action.category)
}
private fun handleFilter(action: RoomListActions.FilterWith) {
setState {
copy(
roomFilter = action.filter
)
}
}
private fun observeRoomSummaries() {
homeRoomListObservableSource
.observe()
.observeOn(Schedulers.computation())
.map {
it.sortedWith(chronologicalRoomComparator)
}
.execute { asyncRooms ->
copy(asyncRooms = asyncRooms)
}
@ -201,9 +213,10 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
}
val roomComparator = when (displayMode) {
RoomListFragment.DisplayMode.HOME -> chronologicalRoomComparator
RoomListFragment.DisplayMode.PEOPLE -> chronologicalRoomComparator
RoomListFragment.DisplayMode.ROOMS -> chronologicalRoomComparator
RoomListFragment.DisplayMode.HOME -> chronologicalRoomComparator
RoomListFragment.DisplayMode.PEOPLE -> chronologicalRoomComparator
RoomListFragment.DisplayMode.ROOMS -> chronologicalRoomComparator
RoomListFragment.DisplayMode.FILTERED -> chronologicalRoomComparator
}
return RoomSummaries().apply {

View File

@ -26,6 +26,7 @@ import im.vector.riotx.R
data class RoomListViewState(
val displayMode: RoomListFragment.DisplayMode,
val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val roomFilter: String = "",
val asyncFilteredRooms: Async<RoomSummaries> = Uninitialized,
// List of roomIds that the user wants to join
val joiningRoomsIds: Set<String> = emptySet(),

View File

@ -18,37 +18,70 @@ package im.vector.riotx.features.home.room.list
import androidx.annotation.StringRes
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.filtered.FilteredRoomFooterItem
import im.vector.riotx.features.home.room.filtered.filteredRoomFooterItem
import javax.inject.Inject
class RoomSummaryController @Inject constructor(private val stringProvider: StringProvider,
private val roomSummaryItemFactory: RoomSummaryItemFactory
private val roomSummaryItemFactory: RoomSummaryItemFactory,
private val roomListNameFilter: RoomListNameFilter
) : TypedEpoxyController<RoomListViewState>() {
var listener: Listener? = null
override fun buildModels(viewState: RoomListViewState) {
val roomSummaries = viewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) {
return@forEach
} else {
val isExpanded = viewState.isCategoryExpanded(category)
buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) {
listener?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries,
viewState.joiningRoomsIds,
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds)
if (viewState.displayMode == RoomListFragment.DisplayMode.FILTERED) {
buildFilteredRooms(viewState)
} else {
val roomSummaries = viewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) {
return@forEach
} else {
val isExpanded = viewState.isCategoryExpanded(category)
buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) {
listener?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries,
viewState.joiningRoomsIds,
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds)
}
}
}
}
}
private fun buildFilteredRooms(viewState: RoomListViewState) {
val summaries = viewState.asyncRooms() ?: return
roomListNameFilter.filter = viewState.roomFilter
val filteredSummaries = summaries
.filter { it.membership == Membership.JOIN && roomListNameFilter.test(it) }
buildRoomModels(filteredSummaries,
viewState.joiningRoomsIds,
viewState.joiningErrorRoomsIds,
viewState.rejectingRoomsIds,
viewState.rejectingErrorRoomsIds)
addFilterFooter(viewState)
}
private fun addFilterFooter(viewState: RoomListViewState) {
filteredRoomFooterItem {
id("filter_footer")
listener(listener)
currentFilter(viewState.roomFilter)
}
}
private fun buildRoomCategory(viewState: RoomListViewState,
summaries: List<RoomSummary>,
@StringRes titleRes: Int,
@ -89,7 +122,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
}
}
interface Listener {
interface Listener : FilteredRoomFooterItem.FilteredRoomFooterItemListener {
fun onToggleRoomCategory(roomCategory: RoomCategory)
fun onRoomSelected(room: RoomSummary)
fun onRejectRoomInvitation(room: RoomSummary)

View File

@ -92,7 +92,7 @@ class FabMenuView @JvmOverloads constructor(context: Context, attrs: AttributeSe
interface Listener {
fun createDirectChat()
fun openRoomDirectory()
fun openRoomDirectory(initialFilter: String = "")
}
}

View File

@ -22,7 +22,7 @@ import android.os.Bundle
import android.widget.Toast
import androidx.core.view.isVisible
import arrow.core.Try
import com.jakewharton.rxbinding2.widget.RxTextView
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
@ -127,9 +127,9 @@ class LoginActivity : VectorBaseActivity() {
private fun setupAuthButton() {
Observable
.combineLatest(
RxTextView.textChanges(loginField).map { it.trim().isNotEmpty() },
RxTextView.textChanges(passwordField).map { it.trim().isNotEmpty() },
RxTextView.textChanges(homeServerField).map { it.trim().isNotEmpty() },
loginField.textChanges().map { it.trim().isNotEmpty() },
passwordField.textChanges().map { it.trim().isNotEmpty() },
homeServerField.textChanges().map { it.trim().isNotEmpty() },
Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty ->
isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty
}

View File

@ -27,7 +27,9 @@ import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
import im.vector.riotx.features.debug.DebugMenuActivity
import im.vector.riotx.features.home.room.detail.RoomDetailActivity
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
import im.vector.riotx.features.settings.VectorSettingsActivity
import timber.log.Timber
@ -56,8 +58,18 @@ class DefaultNavigator @Inject constructor() : Navigator {
context.startActivity(intent)
}
override fun openRoomDirectory(context: Context) {
val intent = Intent(context, RoomDirectoryActivity::class.java)
override fun openRoomDirectory(context: Context, initialFilter: String) {
val intent = RoomDirectoryActivity.getIntent(context, initialFilter)
context.startActivity(intent)
}
override fun openCreateRoom(context: Context, initialName: String) {
val intent = CreateRoomActivity.getIntent(context, initialName)
context.startActivity(intent)
}
override fun openRoomsFiltering(context: Context) {
val intent = FilteredRoomsActivity.newIntent(context)
context.startActivity(intent)
}

View File

@ -27,7 +27,11 @@ interface Navigator {
fun openRoomPreview(publicRoom: PublicRoom, context: Context)
fun openRoomDirectory(context: Context)
fun openCreateRoom(context: Context, initialName: String = "")
fun openRoomDirectory(context: Context, initialFilter: String = "")
fun openRoomsFiltering(context: Context)
fun openSettings(context: Context)

View File

@ -70,7 +70,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
val bodyPreview = event.type
return SimpleNotifiableEvent(
session.sessionParams.credentials.userId,
session.myUserId,
eventId = event.eventId!!,
noisy = false,//will be updated
timestamp = event.originServerTs ?: System.currentTimeMillis(),
@ -109,7 +109,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
roomId = event.root.roomId!!,
roomName = roomName)
notifiableEvent.matrixID = session.sessionParams.credentials.userId
notifiableEvent.matrixID = session.myUserId
return notifiableEvent
} else {
if (event.root.isEncrypted() && event.root.mxDecryptionResult == null) {
@ -145,7 +145,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
roomName = roomName,
roomIsDirect = room.roomSummary()?.isDirect ?: false)
notifiableEvent.matrixID = session.sessionParams.credentials.userId
notifiableEvent.matrixID = session.myUserId
notifiableEvent.soundName = null
// Get the avatars URL
@ -175,7 +175,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
val body = noticeEventFormatter.format(event, dName)
?: stringProvider.getString(R.string.notification_new_invitation)
return InviteNotifiableEvent(
session.sessionParams.credentials.userId,
session.myUserId,
eventId = event.eventId!!,
roomId = roomId,
timestamp = event.originServerTs ?: 0,

View File

@ -121,9 +121,9 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
UUID.randomUUID().toString(),
false,
System.currentTimeMillis(),
session.getUser(session.sessionParams.credentials.userId)?.displayName
session.getUser(session.myUserId)?.displayName
?: context?.getString(R.string.notification_sender_me),
session.sessionParams.credentials.userId,
session.myUserId,
message,
room.roomId,
room.roomSummary()?.displayName ?: room.roomId,

View File

@ -60,7 +60,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
//The first time the notification drawer is refreshed, we force re-render of all notifications
private var firstTime = true
private var eventList = loadEventInfo()
private val eventList = loadEventInfo()
private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
@ -121,11 +121,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
Timber.v("clearMessageEventOfRoom $roomId")
if (roomId != null) {
eventList.removeAll { e ->
if (e is NotifiableMessageEvent) {
return@removeAll e.roomId == roomId
synchronized(eventList) {
eventList.removeAll { e ->
e is NotifiableMessageEvent && e.roomId == roomId
}
return@removeAll false
}
NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID)
}
@ -150,7 +149,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
fun homeActivityDidResume(matrixID: String?) {
synchronized(eventList) {
eventList.removeAll { e ->
return@removeAll e !is NotifiableMessageEvent //messages are cleared when entering room
// messages are cleared when entering room
e !is NotifiableMessageEvent
}
}
}
@ -158,10 +158,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
fun clearMemberShipNotificationForRoom(roomId: String) {
synchronized(eventList) {
eventList.removeAll { e ->
if (e is InviteNotifiableEvent) {
return@removeAll e.roomId == roomId
}
return@removeAll false
e is InviteNotifiableEvent && e.roomId == roomId
}
}
}
@ -182,9 +179,11 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
private fun refreshNotificationDrawerBg() {
Timber.w("refreshNotificationDrawerBg()")
val session = activeSessionHolder.getActiveSession()
val user = session.getUser(session.sessionParams.credentials.userId)
val myUserDisplayName = user?.displayName ?: session.sessionParams.credentials.userId
val session = activeSessionHolder.getSafeActiveSession() ?: return
val user = session.getUser(session.myUserId)
// myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash
val myUserDisplayName = user?.displayName?.takeIf { it.isNotBlank() } ?: session.myUserId
val myUserAvatarUrl = session.contentUrlResolver().resolveThumbnail(user?.avatarUrl, avatarSize, avatarSize, ContentUrlResolver.ThumbnailMethod.SCALE)
synchronized(eventList) {
@ -345,7 +344,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
for (event in simpleEvents) {
//We build a simple event
if (firstTime || !event.hasBeenDisplayed) {
NotificationUtils.buildSimpleEventNotification(context, event, null, myUserDisplayName)?.let {
NotificationUtils.buildSimpleEventNotification(context, event, null, session.myUserId)?.let {
notifications.add(it)
NotificationUtils.showNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID, it)
event.hasBeenDisplayed = true //we can consider it as displayed
@ -432,18 +431,20 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
fun persistInfo() {
if (eventList.isEmpty()) {
deleteCachedRoomNotifications(context)
return
}
try {
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (!file.exists()) file.createNewFile()
FileOutputStream(file).use {
SecretStoringUtils.securelyStoreObject(eventList, "notificationMgr", it, this.context)
synchronized(eventList) {
if (eventList.isEmpty()) {
deleteCachedRoomNotifications(context)
return
}
try {
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (!file.exists()) file.createNewFile()
FileOutputStream(file).use {
SecretStoringUtils.securelyStoreObject(eventList, "notificationMgr", it, this.context)
}
} catch (e: Throwable) {
Timber.e(e, "## Failed to save cached notification info")
}
} catch (e: Throwable) {
Timber.e(e, "## Failed to save cached notification info")
}
}

View File

@ -204,7 +204,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
var olmVersion = "undefined"
activeSessionHolder.getSafeActiveSession()?.let { session ->
userId = session.sessionParams.credentials.userId
userId = session.myUserId
deviceId = session.sessionParams.credentials.deviceId ?: "undefined"
olmVersion = session.getCryptoVersion(context, true)
}

View File

@ -25,14 +25,13 @@ import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding2.widget.RxTextView
import com.jakewharton.rxbinding3.appcompat.queryTextChanges
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.themes.ThemeUtils
import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.fragment_public_rooms.*
import timber.log.Timber
@ -70,9 +69,7 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
it.setDisplayHomeAsUpEnabled(true)
}
publicRoomsFilter.setBackgroundResource(ThemeUtils.getResourceId(requireContext(), R.drawable.bg_search_edit_text_light))
RxTextView.textChanges(publicRoomsFilter)
publicRoomsFilter.queryTextChanges()
.debounce(500, TimeUnit.MILLISECONDS)
.subscribeBy {
viewModel.filterWith(it.toString())
@ -147,6 +144,11 @@ class PublicRoomsFragment : VectorBaseFragment(), PublicRoomsController.Callback
}
override fun invalidate() = withState(viewModel) { state ->
if (publicRoomsFilter.query.toString() != state.currentFilter) {
// For initial filter
publicRoomsFilter.setQuery(state.currentFilter, false)
}
// Populate list with Epoxy
publicRoomsController.setData(state)
}

Some files were not shown because too many files have changed in this diff Show More