diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 44aa2c38..4d12a479 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -47,9 +47,7 @@ android { // Set to true to log privacy or sensible data, such as token // TODO Set to false buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" - // Set to BODY instead of NONE to enable logging - //TODO Revert BODY buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.HEADERS" } @@ -93,6 +91,7 @@ dependencies { def moshi_version = '1.8.0' def lifecycle_version = '2.0.0' def coroutines_version = "1.0.1" + def markwon_version = '3.0.0' implementation fileTree(dir: 'libs', include: ['*.aar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" @@ -114,6 +113,8 @@ dependencies { implementation "com.squareup.moshi:moshi-adapters:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" + implementation "ru.noties.markwon:core:$markwon_version" + // Database implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1' diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt index 9d2edd74..339b435b 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt @@ -22,7 +22,6 @@ 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.toContent import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.MyMembership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType @@ -78,7 +77,7 @@ object RoomDataHelper { fun fakeInitialSync(monarchy: Monarchy, roomId: String) { monarchy.runTransactionSync { realm -> val roomEntity = realm.createObject(roomId) - roomEntity.membership = MyMembership.JOINED + roomEntity.membership = Membership.JOIN val eventList = createFakeListOfEvents(10) val chunkEntity = realm.createObject().apply { nextToken = null diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt index c43ff438..221887c7 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -20,7 +20,9 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor +import im.vector.matrix.android.internal.database.model.SessionRealmModule +import im.vector.matrix.android.internal.session.room.EventRelationExtractor +import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor @@ -46,7 +48,9 @@ internal class TimelineTest : InstrumentedTest { fun setup() { Timber.plant(Timber.DebugTree()) Realm.init(context()) - val testConfiguration = RealmConfiguration.Builder().name("test-realm").build() + val testConfiguration = RealmConfiguration.Builder().name("test-realm") + .modules(SessionRealmModule()).build() + Realm.deleteRealm(testConfiguration) monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build() RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID) @@ -58,8 +62,16 @@ internal class TimelineTest : InstrumentedTest { val paginationTask = FakePaginationTask(tokenChunkEventPersistor) val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) - val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) - return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null) + val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor()) + return DefaultTimeline( + ROOM_ID, + initialEventId, + monarchy.realmConfiguration, + taskExecutor, + getContextOfEventTask, + timelineEventFactory, + paginationTask, + null) } @Test diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt index 9557434c..19dc37e6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/MatrixPatterns.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.api -import java.util.* import java.util.regex.Pattern /** @@ -25,53 +24,53 @@ import java.util.regex.Pattern object MatrixPatterns { // Note: TLD is not mandatory (localhost, IP address...) - private val DOMAIN_REGEX = ":[A-Z0-9.-]+(:[0-9]{2,5})?" + private const val DOMAIN_REGEX = ":[A-Z0-9.-]+(:[0-9]{2,5})?" // regex pattern to find matrix user ids in a string. // See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids - private val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX" - val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = Pattern.compile(MATRIX_USER_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE) + private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = Pattern.compile(MATRIX_USER_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE) // regex pattern to find room ids in a string. - private val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX" - val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = Pattern.compile(MATRIX_ROOM_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE) + private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = Pattern.compile(MATRIX_ROOM_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE) // regex pattern to find room aliases in a string. - private val MATRIX_ROOM_ALIAS_REGEX = "#[A-Z0-9._%#@=+-]+$DOMAIN_REGEX" - val PATTERN_CONTAIN_MATRIX_ALIAS = Pattern.compile(MATRIX_ROOM_ALIAS_REGEX, Pattern.CASE_INSENSITIVE) + private const val MATRIX_ROOM_ALIAS_REGEX = "#[A-Z0-9._%#@=+-]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_ALIAS = Pattern.compile(MATRIX_ROOM_ALIAS_REGEX, Pattern.CASE_INSENSITIVE) // regex pattern to find message ids in a string. - private val MATRIX_EVENT_IDENTIFIER_REGEX = "\\$[A-Z0-9]+$DOMAIN_REGEX" - val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = Pattern.compile(MATRIX_EVENT_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE) + private const val MATRIX_EVENT_IDENTIFIER_REGEX = "\\$[A-Z0-9]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = Pattern.compile(MATRIX_EVENT_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE) // regex pattern to find message ids in a string. - private val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$[A-Z0-9/+]+" - val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = Pattern.compile(MATRIX_EVENT_IDENTIFIER_V3_REGEX, Pattern.CASE_INSENSITIVE) + private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$[A-Z0-9/+]+" + private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = Pattern.compile(MATRIX_EVENT_IDENTIFIER_V3_REGEX, Pattern.CASE_INSENSITIVE) // regex pattern to find group ids in a string. - private val MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+$DOMAIN_REGEX" - val PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = Pattern.compile(MATRIX_GROUP_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE) + private const val MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+$DOMAIN_REGEX" + private val PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = Pattern.compile(MATRIX_GROUP_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE) // regex pattern to find permalink with message id. // Android does not support in URL so extract it. - private val PERMALINK_BASE_REGEX = "https://matrix\\.to/#/" - private val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/" - val SEP_REGEX = "/" + private const val PERMALINK_BASE_REGEX = "https://matrix\\.to/#/" + private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/" + const val SEP_REGEX = "/" - private val LINK_TO_ROOM_ID_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX - val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = Pattern.compile(LINK_TO_ROOM_ID_REGEXP, Pattern.CASE_INSENSITIVE) + private const val LINK_TO_ROOM_ID_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX + private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = Pattern.compile(LINK_TO_ROOM_ID_REGEXP, Pattern.CASE_INSENSITIVE) - private val LINK_TO_ROOM_ALIAS_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX - val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = Pattern.compile(LINK_TO_ROOM_ALIAS_REGEXP, Pattern.CASE_INSENSITIVE) + private const val LINK_TO_ROOM_ALIAS_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX + private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = Pattern.compile(LINK_TO_ROOM_ALIAS_REGEXP, Pattern.CASE_INSENSITIVE) - private val LINK_TO_APP_ROOM_ID_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX - val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = Pattern.compile(LINK_TO_APP_ROOM_ID_REGEXP, Pattern.CASE_INSENSITIVE) + private const val LINK_TO_APP_ROOM_ID_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX + private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = Pattern.compile(LINK_TO_APP_ROOM_ID_REGEXP, Pattern.CASE_INSENSITIVE) - private val LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX - val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = Pattern.compile(LINK_TO_APP_ROOM_ALIAS_REGEXP, Pattern.CASE_INSENSITIVE) + private const val LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX + private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = Pattern.compile(LINK_TO_APP_ROOM_ALIAS_REGEXP, Pattern.CASE_INSENSITIVE) // list of patterns to find some matrix item. - val MATRIX_PATTERNS = Arrays.asList( + val MATRIX_PATTERNS = listOf( PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID, PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS, PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID, @@ -133,4 +132,4 @@ object MatrixPatterns { fun isGroupId(str: String?): Boolean { return str != null && PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER.matcher(str).matches() } -}// Cannot be instantiated +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt index bca7d480..f231d3f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/HomeServerConnectionConfig.kt @@ -70,11 +70,11 @@ data class HomeServerConnectionConfig( if (hsUri.scheme != "http" && hsUri.scheme != "https") { throw RuntimeException("Invalid home server URI: " + hsUri) } - // remove trailing / - homeServerUri = if (hsUri.toString().endsWith("/")) { + // ensure trailing / + homeServerUri = if (!hsUri.toString().endsWith("/")) { try { val url = hsUri.toString() - Uri.parse(url.substring(0, url.length - 1)) + Uri.parse("$url/") } catch (e: Exception) { throw RuntimeException("Invalid home server URI: $hsUri") } @@ -96,11 +96,11 @@ data class HomeServerConnectionConfig( if (identityServerUri.scheme != "http" && identityServerUri.scheme != "https") { throw RuntimeException("Invalid identity server URI: $identityServerUri") } - // remove trailing / - if (identityServerUri.toString().endsWith("/")) { + // ensure trailing / + if (!identityServerUri.toString().endsWith("/")) { try { val url = identityServerUri.toString() - this.identityServerUri = Uri.parse(url.substring(0, url.length - 1)) + this.identityServerUri = Uri.parse("$url/") } catch (e: Exception) { throw RuntimeException("Invalid identity server URI: $identityServerUri") } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt index d09db8c4..eed9b7b6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt @@ -17,9 +17,6 @@ package im.vector.matrix.android.api.permalinks import android.text.Spannable -import android.text.SpannableString -import android.text.method.LinkMovementMethod -import android.widget.TextView import im.vector.matrix.android.api.MatrixPatterns /** @@ -56,37 +53,5 @@ object MatrixLinkify { } return hasMatch } - - fun addLinks(textView: TextView, callback: MatrixPermalinkSpan.Callback?): Boolean { - val text = textView.text - if (text is Spannable) { - if (addLinks(text, callback)) { - addLinkMovementMethod(textView) - return true - } - - return false - } else { - val spannableString = SpannableString.valueOf(text) - if (addLinks(spannableString, callback)) { - addLinkMovementMethod(textView) - textView.text = spannableString - return true - } - return false - } - } - - /** - * Add linkMovementMethod on textview if not already set - * @param textView the textView on which the movementMethod is set - */ - fun addLinkMovementMethod(textView: TextView) { - val movementMethod = textView.movementMethod - if (movementMethod == null || movementMethod !is LinkMovementMethod) { - if (textView.linksClickable) { - textView.movementMethod = LinkMovementMethod.getInstance() - } - } - } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 742adb90..82264306 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.group.GroupService +import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService @@ -34,6 +35,7 @@ import im.vector.matrix.android.api.session.user.UserService */ interface Session : RoomService, + RoomDirectoryService, GroupService, UserService, CryptoService, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/AggregatedAnnotation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/AggregatedAnnotation.kt new file mode 100644 index 00000000..3f1c6199 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/AggregatedAnnotation.kt @@ -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.matrix.android.api.session.events.model + +import com.squareup.moshi.JsonClass + +/** + * + * { + * "chunk": [ + * { + * "type": "m.reaction", + * "key": "๐Ÿ‘", + * "count": 3 + * } + * ], + * "limited": false, + * "count": 1 + * }, + * + */ + +@JsonClass(generateAdapter = true) +data class AggregatedAnnotation ( + override val limited: Boolean? = false, + override val count: Int? = 0, + val chunk: List? = null + +) : UnsignedRelationInfo \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/AggregatedRelations.kt new file mode 100644 index 00000000..0f8d21f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/AggregatedRelations.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.events.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * + * { + * "m.annotation": { + * "chunk": [ + * { + * "type": "m.reaction", + * "key": "๐Ÿ‘", + * "count": 3 + * } + * ], + * "limited": false, + * "count": 1 + * }, + * "m.reference": { + * "chunk": [ + * { + * "type": "m.room.message", + * "event_id": "$some_event_id" + * } + * ], + * "limited": false, + * "count": 1 + * } + * } + * + */ + +@JsonClass(generateAdapter = true) +data class AggregatedRelations( + @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null, + @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/DefaultUnsignedRelationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/DefaultUnsignedRelationInfo.kt new file mode 100644 index 00000000..3e2df0aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/DefaultUnsignedRelationInfo.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.events.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DefaultUnsignedRelationInfo( + override val limited: Boolean? = false, + override val count: Int? = 0, + val chunk: List>? = null + +) : UnsignedRelationInfo \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index a815116c..7289b023 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -31,11 +31,20 @@ typealias Content = JsonDict /** * This methods is a facility method to map a json content to a model. */ -inline fun Content?.toModel(): T? { +inline fun Content?.toModel(catchError: Boolean = true): T? { return this?.let { val moshi = MoshiProvider.providesMoshi() val moshiAdapter = moshi.adapter(T::class.java) - return moshiAdapter.fromJsonValue(it) + return try { + moshiAdapter.fromJsonValue(it) + } catch (e: Exception) { + if (catchError) { + Timber.e(e, "To model failed : $e") + null + } else { + throw e + } + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index b867063c..98ce6c0f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -74,6 +74,9 @@ object EventType { const val KEY_VERIFICATION_MAC = "m.key.verification.mac" const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel" + // Relation Events + const val REACTION = "m.reaction" + private val STATE_EVENTS = listOf( STATE_ROOM_NAME, STATE_ROOM_TOPIC, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationChunkInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationChunkInfo.kt new file mode 100644 index 00000000..f4f1d866 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationChunkInfo.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.events.model + +import com.squareup.moshi.JsonClass + +/** + * + * { + * "type": "m.reaction", + * "key": "๐Ÿ‘", + * "count": 3 + * } + * + */ + +@JsonClass(generateAdapter = true) +data class RelationChunkInfo( + val type: String, + val key: String, + val count: Int +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt new file mode 100644 index 00000000..56d4801c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.events.model + + +/** + * Constants defining known event relation types from Matrix specifications. + */ +object RelationType { + + /** Lets you define an event which annotates an existing event.*/ + 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.*/ + const val REFERENCE = "m.reference" + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt index 6a2ea226..4a9547e3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt @@ -24,5 +24,6 @@ data class UnsignedData( @Json(name = "age") val age: Long?, @Json(name = "redacted_because") val redactedEvent: Event? = null, @Json(name = "transaction_id") val transactionId: String? = null, - @Json(name = "prev_content") val prevContent: Map? = null + @Json(name = "prev_content") val prevContent: Map? = null, + @Json(name = "m.relations") val relations: AggregatedRelations? = null ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedRelationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedRelationInfo.kt new file mode 100644 index 00000000..5b627b62 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedRelationInfo.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.events.model + + +interface UnsignedRelationInfo { + val limited : Boolean? + val count: Int? +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index 9cb4b846..fe6a5443 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -18,8 +18,9 @@ package im.vector.matrix.android.api.session.room import androidx.lifecycle.LiveData import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService -import im.vector.matrix.android.api.session.room.members.RoomMembersService +import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService @@ -28,11 +29,13 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineService /** * This interface defines methods to interact within a room. */ -interface Room : TimelineService, +interface Room : + TimelineService, SendService, ReadService, - RoomMembersService, + MembershipService, StateService, + RelationService, RoomCryptoService { /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt new file mode 100644 index 00000000..5b66ddd8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomDirectoryService.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room + +import im.vector.matrix.android.api.MatrixCallback +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.api.util.Cancelable + +/** + * This interface defines methods to get and join public rooms. It's implemented at the session level. + */ +interface RoomDirectoryService { + + /** + * Get rooms from directory + */ + fun getPublicRooms(server: String?, + publicRoomsParams: PublicRoomsParams, + callback: MatrixCallback): Cancelable + + /** + * Join a room by id + */ + fun joinRoom(roomId: String, + callback: MatrixCallback) + + /** + * Fetches the overall metadata about protocols supported by the homeserver. + * Includes both the available protocols and all fields required for queries against each protocol. + */ + fun getThirdPartyProtocol(callback: MatrixCallback>) + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt similarity index 84% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt index 930afd7a..59054dc1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/MembershipService.kt @@ -24,9 +24,9 @@ import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.util.Cancelable /** - * This interface defines methods to retrieve room members of a room. It's implemented at the room level. + * This interface defines methods to handling membership. It's implemented at the room level. */ -interface RoomMembersService { +interface MembershipService { /** * This methods load all room members if it was done yet. @@ -54,4 +54,15 @@ interface RoomMembersService { */ fun invite(userId: String, callback: MatrixCallback) + /** + * Join the room + */ + fun join(callback: MatrixCallback) + + /** + * Leave the room. + * + */ + fun leave(callback: MatrixCallback) + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EditAggregatedSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EditAggregatedSummary.kt new file mode 100644 index 00000000..759ba1ec --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EditAggregatedSummary.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.room.model + +import im.vector.matrix.android.api.session.events.model.Content + +data class EditAggregatedSummary( + val aggregatedContent: Content? = null, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + val sourceEvents: List, + val localEchos: List, + val lastEditTs: Long = 0 +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/MyMembership.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt similarity index 78% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/MyMembership.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt index cd9c3932..18899f87 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/MyMembership.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt @@ -13,15 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package im.vector.matrix.android.api.session.room.model -/** - * Represents the membership of the current auth user on a room. - */ -enum class MyMembership { - JOINED, - LEFT, - INVITED, - NONE -} \ No newline at end of file +data class EventAnnotationsSummary( + var eventId: String, + var reactionsSummary: List, + var editSummary: EditAggregatedSummary? +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt index 09d7a7de..d87bb9b3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/Membership.kt @@ -19,10 +19,12 @@ package im.vector.matrix.android.api.session.room.model import com.squareup.moshi.Json /** - * Represents the membership of a user on a room. Linked to a [RoomMember] + * Represents the membership of a user on a room */ enum class Membership(val value: String) { + NONE("none"), + @Json(name = "invite") INVITE("invite"), @@ -38,4 +40,8 @@ enum class Membership(val value: String) { @Json(name = "ban") BAN("ban"); + fun isLeft(): Boolean { + return this == KNOCK || this == LEAVE || this == BAN + } + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt new file mode 100644 index 00000000..1fbe58c7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReactionAggregatedSummary.kt @@ -0,0 +1,10 @@ +package im.vector.matrix.android.api.session.room.model + +data class ReactionAggregatedSummary( + val key: String, // "๐Ÿ‘" + val count: Int, // 8 + val addedByMe: Boolean, // true + val firstTimestamp: Long, // unix timestamp + val sourceEvents: List, + val localEchoEvents: List +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 429d7882..f6de3589 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -31,7 +31,8 @@ data class RoomSummary( val isDirect: Boolean = false, val lastMessage: Event? = null, val otherMemberIds: List = emptyList(), - var notificationCount: Int = 0, - var highlightCount: Int = 0, - var tags: List = emptyList() + val notificationCount: Int = 0, + val highlightCount: Int = 0, + val tags: List = emptyList(), + val membership: Membership = Membership.NONE ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/FileInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/FileInfo.kt index 2a115a58..16dc4d23 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/FileInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/FileInfo.kt @@ -21,7 +21,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class FileInfo( - @Json(name = "mimetype") val mimeType: String, + @Json(name = "mimetype") val mimeType: String?, @Json(name = "size") val size: Long = 0, @Json(name = "thumbnail_info") val thumbnailInfo: ThumbnailInfo? = null, @Json(name = "thumbnail_url") val thumbnailUrl: String? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt index ffae8eef..9b33b007 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageAudioContent.kt @@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageAudioContent( @Json(name = "msgtype") override val type: String, @Json(name = "body") override val body: String, @Json(name = "info") val info: AudioInfo? = null, - @Json(name = "url") val url: String? = null + @Json(name = "url") val url: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null ) : MessageContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt index 1d6880b0..c45e47fc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageContent.kt @@ -16,8 +16,13 @@ package im.vector.matrix.android.api.session.room.model.message +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent + interface MessageContent { val type: String val body: String + val relatesTo: RelationDefaultContent? + val newContent: Content? } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageDefaultContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageDefaultContent.kt index a924efe4..45ce9542 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageDefaultContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageDefaultContent.kt @@ -18,9 +18,13 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageDefaultContent( @Json(name = "msgtype") override val type: String, - @Json(name = "body") override val body: String + @Json(name = "body") override val body: String, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null ) : MessageContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt index bae9c96f..88fd3bc1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageEmoteContent.kt @@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageEmoteContent( @Json(name = "msgtype") override val type: String, @Json(name = "body") override val body: String, @Json(name = "format") val format: String? = null, - @Json(name = "formatted_body") val formattedBody: String? = null + @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null ) : MessageContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt index 505aee7a..8f58294c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageFileContent.kt @@ -18,6 +18,8 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageFileContent( @@ -25,5 +27,7 @@ data class MessageFileContent( @Json(name = "body") override val body: String, @Json(name = "filename") val filename: String? = null, @Json(name = "info") val info: FileInfo? = null, - @Json(name = "url") val url: String? = null + @Json(name = "url") val url: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null ) : MessageContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt index a3e2f661..2c978b97 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageImageContent.kt @@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageImageContent( @Json(name = "msgtype") override val type: String, @Json(name = "body") override val body: String, @Json(name = "info") val info: ImageInfo? = null, - @Json(name = "url") val url: String? = null + @Json(name = "url") val url: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null ) : MessageContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt index b17bfae1..ddd67af9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt @@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageLocationContent( @Json(name = "msgtype") override val type: String, @Json(name = "body") override val body: String, @Json(name = "geo_uri") val geoUri: String, - @Json(name = "info") val info: LocationInfo? = null + @Json(name = "info") val info: LocationInfo? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null ) : MessageContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt index 4db1f4a0..54037c60 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageNoticeContent.kt @@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageNoticeContent( @Json(name = "msgtype") override val type: String, @Json(name = "body") override val body: String, @Json(name = "format") val format: String? = null, - @Json(name = "formatted_body") val formattedBody: String? = null + @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null ) : MessageContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt index 084b5dde..3256d830 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageTextContent.kt @@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageTextContent( @Json(name = "msgtype") override val type: String, @Json(name = "body") override val body: String, @Json(name = "format") val format: String? = null, - @Json(name = "formatted_body") val formattedBody: String? = null + @Json(name = "formatted_body") val formattedBody: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null ) : MessageContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt index 0b239f6c..7db33018 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt @@ -18,16 +18,16 @@ package im.vector.matrix.android.api.session.room.model.message object MessageType { - val MSGTYPE_TEXT = "m.text" - val MSGTYPE_EMOTE = "m.emote" - val MSGTYPE_NOTICE = "m.notice" - val MSGTYPE_IMAGE = "m.image" - val MSGTYPE_AUDIO = "m.audio" - val MSGTYPE_VIDEO = "m.video" - val MSGTYPE_LOCATION = "m.location" - val MSGTYPE_FILE = "m.file" - val FORMAT_MATRIX_HTML = "org.matrix.custom.html" + const val MSGTYPE_TEXT = "m.text" + const val MSGTYPE_EMOTE = "m.emote" + const val MSGTYPE_NOTICE = "m.notice" + const val MSGTYPE_IMAGE = "m.image" + const val MSGTYPE_AUDIO = "m.audio" + const val MSGTYPE_VIDEO = "m.video" + const val MSGTYPE_LOCATION = "m.location" + const val MSGTYPE_FILE = "m.file" + const val FORMAT_MATRIX_HTML = "org.matrix.custom.html" // Add, in local, a fake message type in order to StickerMessage can inherit Message class // Because sticker isn't a message type but a event type without msgtype field - val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" + const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker" } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt index 319a9493..40c29942 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVideoContent.kt @@ -18,11 +18,15 @@ package im.vector.matrix.android.api.session.room.model.message import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent @JsonClass(generateAdapter = true) data class MessageVideoContent( @Json(name = "msgtype") override val type: String, @Json(name = "body") override val body: String, @Json(name = "info") val info: VideoInfo? = null, - @Json(name = "url") val url: String? = null + @Json(name = "url") val url: String? = null, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null ) : MessageContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionContent.kt new file mode 100644 index 00000000..c7a86631 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionContent.kt @@ -0,0 +1,9 @@ +package im.vector.matrix.android.api.session.room.model.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReactionContent( + @Json(name = "m.relates_to") val relatesTo: ReactionInfo? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt new file mode 100644 index 00000000..3a2f6169 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt @@ -0,0 +1,13 @@ +package im.vector.matrix.android.api.session.room.model.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReactionInfo( + @Json(name = "rel_type") override val type: String?, + @Json(name = "event_id") override val eventId: String, + val key: String, + //always null for reaction + @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null +) : RelationContent \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt new file mode 100644 index 00000000..3f60af7c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt @@ -0,0 +1,7 @@ +package im.vector.matrix.android.api.session.room.model.relation + +interface RelationContent { + val type: String? + val eventId: String? + val inReplyTo: ReplyToContent? +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt new file mode 100644 index 00000000..853a3817 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.room.model.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RelationDefaultContent( + @Json(name = "rel_type") override val type: String?, + @Json(name = "event_id") override val eventId: String?, + @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null +) : RelationContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt new file mode 100644 index 00000000..bc924728 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationService.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.room.model.relation + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.util.Cancelable + +/** + * In some cases, events may wish to reference other events. + * This could be to form a thread of messages for the user to follow along with, + * or to provide more context as to what a particular event is describing. + * Relation are used to associate new information with an existing event. + * + * Relations are events which have an m.relates_to mixin in their contents, + * and the new information they convey is expressed in their usual event type and content. + * + * Three types of relations are defined, each defining different behaviour when aggregated: + * + * m.annotation - lets you define an event which annotates an existing event. + * When aggregated, groups events together based on key and returns a count. + * (aka SQL's COUNT) These are primarily intended for handling reactions. + * + * m.replace - lets you define an event which replaces an existing event. + * When aggregated, returns the most recent replacement event. (aka SQL's MAX) + * These are primarily intended for handling edits. + * + * m.reference - lets you define an event which references an existing event. + * When aggregated, currently doesn't do anything special, but in future could bundle chains of references (i.e. threads). + * These are primarily intended for handling replies (and in future threads). + */ +interface RelationService { + + + /** + * Sends a reaction (emoji) to the targetedEvent. + * @param reaction the reaction (preferably emoji) + * @param targetEventId the id of the event being reacted + */ + fun sendReaction(reaction: String, targetEventId: String): Cancelable + + + /** + * Undo a reaction (emoji) to the targetedEvent. + * @param reaction the reaction (preferably emoji) + * @param targetEventId the id of the event being reacted + * @param myUserId used to know if a reaction event was made by the user + */ + fun undoReaction(reaction: String, targetEventId: String, myUserId: String)//: Cancelable + + + /** + * Update a quick reaction (toggle). + * If you have reacted with agree and then you click on disagree, this call will delete(redact) + * the disagree and add the agree + * If you click on a reaction that you already reacted with, it will undo it + * @param reaction the reaction (preferably emoji) + * @param oppositeReaction the opposite reaction(preferably emoji) + * @param targetEventId the id of the event being reacted + * @param myUserId used to know if a reaction event was made by the user + */ + fun updateQuickReaction(reaction: String, oppositeReaction: String, targetEventId: String, myUserId: String) + + + /** + * Edit a text message body. Limited to "m.text" contentType + * @param targetEventId The event to edit + * @param newBodyText The edited body + * @param compatibilityBodyText The text that will appear on clients that don't support yet edition + */ + fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String = "* $newBodyText"): Cancelable + + + /** + * Reply to an event in the timeline (must be in same room) + * https://matrix.org/docs/spec/client_server/r0.4.0.html#id350 + * @param eventReplied the event referenced by the reply + * @param replyText the reply text + */ + fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt new file mode 100644 index 00000000..3df8a534 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReplyToContent.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.model.relation + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ReplyToContent( + @Json(name = "event_id") val eventId: String +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoom.kt new file mode 100644 index 00000000..3ed3ec0c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoom.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2014 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.room.model.roomdirectory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the objects returned by /publicRooms call. + */ +@JsonClass(generateAdapter = true) +data class PublicRoom( + + /** + * Aliases of the room. May be empty. + */ + @Json(name = "aliases") + var aliases: List? = null, + + /** + * The canonical alias of the room, if any. + */ + @Json(name = "canonical_alias") + var canonicalAlias: String? = null, + + /** + * The name of the room, if any. + */ + @Json(name = "name") + var name: String? = null, + + /** + * Required. The number of members joined to the room. + */ + @Json(name = "num_joined_members") + var numJoinedMembers: Int = 0, + + /** + * Required. The ID of the room. + */ + @Json(name = "room_id") + var roomId: String, + + /** + * The topic of the room, if any. + */ + @Json(name = "topic") + var topic: String? = null, + + /** + * Required. Whether the room may be viewed by guest users without joining. + */ + @Json(name = "world_readable") + var worldReadable: Boolean = false, + + /** + * Required. Whether guest users may join the room and participate in it. If they can, they will be subject to ordinary power level rules like any other user. + */ + @Json(name = "guest_can_join") + var guestCanJoin: Boolean = false, + + /** + * The URL for the room's avatar, if one is set. + */ + @Json(name = "avatar_url") + var avatarUrl: String? = null, + + /** + * Undocumented item + */ + @Json(name = "m.federate") + var isFederated: Boolean = false + +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsFilter.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsFilter.kt new file mode 100644 index 00000000..b4de72e4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsFilter.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2014 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.room.model.roomdirectory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to define a filter to retrieve public rooms + */ +@JsonClass(generateAdapter = true) +data class PublicRoomsFilter( + /** + * A string to search for in the room metadata, e.g. name, topic, canonical alias etc. (Optional). + */ + @Json(name = "generic_search_term") + var searchTerm: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsParams.kt new file mode 100644 index 00000000..e2af1c3c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsParams.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.room.model.roomdirectory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class to pass parameters to get the public rooms list + */ +@JsonClass(generateAdapter = true) +data class PublicRoomsParams( + /** + * Limit the number of results returned. + */ + @Json(name = "limit") + var limit: Int? = null, + + /** + * A pagination token from a previous request, allowing clients to get the next (or previous) batch of rooms. + * The direction of pagination is specified solely by which token is supplied, rather than via an explicit flag. + */ + @Json(name = "since") + var since: String? = null, + + /** + * Filter to apply to the results. + */ + @Json(name = "filter") + var filter: PublicRoomsFilter? = null, + + /** + * Whether or not to include all known networks/protocols from application services on the homeserver. Defaults to false. + */ + @Json(name = "include_all_networks") + var includeAllNetworks: Boolean = false, + + /** + * The specific third party network/protocol to request from the homeserver. Can only be used if include_all_networks is false. + */ + @Json(name = "third_party_instance_id") + var thirdPartyInstanceId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsResponse.kt new file mode 100644 index 00000000..3799d097 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/roomdirectory/PublicRoomsResponse.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2014 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.room.model.roomdirectory + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the public rooms request response + */ +@JsonClass(generateAdapter = true) +data class PublicRoomsResponse( + /** + * A pagination token for the response. The absence of this token means there are no more results to fetch and the client should stop paginating. + */ + @Json(name = "next_batch") + var nextBatch: String? = null, + + /** + * A pagination token that allows fetching previous results. The absence of this token means there are no results before this batch, + * i.e. this is the first batch. + */ + @Json(name = "prev_batch") + var prevBatch: String? = null, + + /** + * A paginated chunk of public rooms. + */ + @Json(name = "chunk") + var chunk: List? = null, + + /** + * An estimate on the total number of public rooms, if the server has an estimate. + */ + @Json(name = "total_room_count_estimate") + var totalRoomCountEstimate: Int? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/FieldType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/FieldType.kt new file mode 100644 index 00000000..2e9f6956 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/FieldType.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.model.thirdparty + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FieldType( + /** + * Required. A regular expression for validation of a field's value. This may be relatively coarse to verify the value as the application + * service providing this protocol may apply additional + */ + @Json(name = "regexp") + val regexp: String? = null, + + /** + * Required. An placeholder serving as a valid example of the field value. + */ + @Json(name = "placeholder") + val placeholder: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/RoomDirectoryData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/RoomDirectoryData.kt new file mode 100644 index 00000000..11a347c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/RoomDirectoryData.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.model.thirdparty + +/** + * This class describes a rooms directory server. + */ +data class RoomDirectoryData( + + /** + * The server name (might be null) + * Set null when the server is the current user's home server. + */ + val homeServer: String? = null, + + /** + * The display name (the server description) + */ + val displayName: String = DEFAULT_HOME_SERVER_NAME, + + /** + * The third party server identifier + */ + val thirdPartyInstanceId: String? = null, + + /** + * Tell if all the federated servers must be included + */ + val includeAllNetworks: Boolean = false, + + /** + * the avatar url + */ + val avatarUrl: String? = null +) { + + companion object { + const val DEFAULT_HOME_SERVER_NAME = "Matrix" + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocol.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocol.kt new file mode 100644 index 00000000..a31df794 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocol.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.model.thirdparty + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ThirdPartyProtocol( + /** + * Required. Fields which may be used to identify a third party user. These should be ordered to suggest the way that entities may be grouped, + * where higher groupings are ordered first. For example, the name of a network should be searched before the nickname of a user. + */ + @Json(name = "user_fields") + var userFields: List? = null, + + /** + * Required. Fields which may be used to identify a third party location. These should be ordered to suggest the way that + * entities may be grouped, where higher groupings are ordered first. For example, the name of a network should be + * searched before the name of a channel. + */ + @Json(name = "location_fields") + var locationFields: List? = null, + + /** + * Required. A content URI representing an icon for the third party protocol. + * + * FIXDOC: This field was not present in legacy Riot, and it is sometimes sent by the server (no not Required?) + */ + @Json(name = "icon") + var icon: String? = null, + + /** + * Required. The type definitions for the fields defined in the user_fields and location_fields. Each entry in those arrays MUST have an entry here. + * The string key for this object is field name itself. + * + * May be an empty object if no fields are defined. + */ + @Json(name = "field_types") + var fieldTypes: Map? = null, + + /** + * Required. A list of objects representing independent instances of configuration. For example, multiple networks on IRC + * if multiple are provided by the same application service. + */ + @Json(name = "instances") + var instances: List? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt new file mode 100644 index 00000000..d2bea700 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/thirdparty/ThirdPartyProtocolInstance.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.model.thirdparty + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ThirdPartyProtocolInstance( + /** + * Required. A human-readable description for the protocol, such as the name. + */ + @Json(name = "desc") + var desc: String? = null, + + /** + * An optional content URI representing the protocol. Overrides the one provided at the higher level Protocol object. + */ + @Json(name = "icon") + var icon: String? = null, + + /** + * Required. Preset values for fields the client may use to search by. + */ + @Json(name = "fields") + var fields: Map? = null, + + /** + * Required. A unique identifier across all instances. + */ + @Json(name = "network_id") + var networkId: String? = null, + + /** + * FIXDOC Not documented on matrix.org doc + */ + @Json(name = "instance_id") + var instanceId: String? = null, + + + /** + * FIXDOC Not documented on matrix.org doc + */ + @Json(name = "bot_user_id") + var botUserId: String? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index 6a0d672c..875ac75b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.send import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.util.Cancelable @@ -30,9 +31,18 @@ interface SendService { * Method to send a text message asynchronously. * @param text the text message to send * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE + * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present * @return a [Cancelable] */ - fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable + fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable + + /** + * Method to send a text message with a formatted body. + * @param text the text message to send + * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML + * @return a [Cancelable] + */ + fun sendFormattedTextMessage(text: String,formattedText: String): Cancelable /** * Method to send a media asynchronously. @@ -48,5 +58,11 @@ interface SendService { */ fun sendMedias(attachments: List): Cancelable + /** + * Redacts (delete) the given event. + * @param event The event to redact + * @param reason Optional reason string + */ + fun redactEvent(event: Event, reason: String?): Cancelable } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index 280a3565..9ba481a4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.timeline 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.room.model.EventAnnotationsSummary import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.send.SendState @@ -32,7 +33,8 @@ data class TimelineEvent( val displayIndex: Int, val senderName: String?, val senderAvatar: String?, - val sendState: SendState + val sendState: SendState, + val annotations: EventAnnotationsSummary? = null ) { val metadata = HashMap() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt index 8e90e5e9..356ea6cc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/user/UserService.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.user +import androidx.lifecycle.LiveData import im.vector.matrix.android.api.session.user.model.User /** @@ -32,4 +33,11 @@ interface UserService { */ fun getUser(userId: String): User? + /** + * Observe a live user from a userId + * @param userId the userId to look for. + * @return a Livedata of user with userId + */ + fun observeUser(userId: String): LiveData + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt index 95d6ad24..a3b9e58c 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt @@ -39,7 +39,6 @@ import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent import im.vector.matrix.android.api.session.room.model.RoomMember -import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting @@ -62,8 +61,8 @@ import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService import im.vector.matrix.android.internal.di.MoshiProvider -import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask -import im.vector.matrix.android.internal.session.room.members.RoomMembers +import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask +import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.sync.model.SyncResponse import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith @@ -116,20 +115,18 @@ internal class CryptoManager( // Actions private val setDeviceVerificationAction: SetDeviceVerificationAction, private val megolmSessionDataImporter: MegolmSessionDataImporter, - private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - // Repository private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, + // Repository private val megolmEncryptionFactory: MXMegolmEncryptionFactory, private val olmEncryptionFactory: MXOlmEncryptionFactory, - // Tasks private val deleteDeviceTask: DeleteDeviceTask, + // Tasks private val getDevicesTask: GetDevicesTask, private val setDeviceNameTask: SetDeviceNameTask, private val uploadKeysTask: UploadKeysTask, private val loadRoomMembersTask: LoadRoomMembersTask, private val monarchy: Monarchy, private val coroutineDispatchers: MatrixCoroutineDispatchers, - // TaskExecutor private val taskExecutor: TaskExecutor ) : CryptoService { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index 3b729eca..47889721 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -233,12 +233,11 @@ internal class CryptoModule { olmManager = get(), setDeviceVerificationAction = get(), megolmSessionDataImporter = get(), - ensureOlmSessionsForDevicesAction = get(), warnOnUnknownDevicesRepository = get(), megolmEncryptionFactory = get(), olmEncryptionFactory = get(), - // Tasks deleteDeviceTask = get(), + // Tasks getDevicesTask = get(), setDeviceNameTask = get(), uploadKeysTask = get(), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 0db4d14f..1741796b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -86,10 +86,9 @@ internal fun ChunkEntity.add(roomId: String, isUnlinked: Boolean = false) { assertIsManaged() - if (event.eventId.isNullOrEmpty() || this.events.fastContains(event.eventId)) { + if (event.eventId != null && events.fastContains(event.eventId)) { return } - var currentDisplayIndex = lastDisplayIndex(direction, 0) if (direction == PaginationDirection.FORWARDS) { currentDisplayIndex += 1 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt new file mode 100644 index 00000000..8b653b74 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -0,0 +1,36 @@ +package im.vector.matrix.android.internal.database.mapper + +import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary +import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary +import im.vector.matrix.android.api.session.room.model.ReactionAggregatedSummary +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity + +internal object EventAnnotationsSummaryMapper { + fun map(annotationsSummary: EventAnnotationsSummaryEntity): EventAnnotationsSummary { + return EventAnnotationsSummary( + eventId = annotationsSummary.eventId, + reactionsSummary = annotationsSummary.reactionsSummary.toList().map { + ReactionAggregatedSummary( + it.key, + it.count, + it.addedByMe, + it.firstTimestamp, + it.sourceEvents.toList(), + it.sourceLocalEcho.toList() + ) + }, + editSummary = annotationsSummary.editSummary?.let { + EditAggregatedSummary( + ContentMapper.map(it.aggregatedContent), + it.sourceEvents.toList(), + it.sourceLocalEchoEvents.toList(), + it.lastEditTs + ) + } + ) + } +} + +internal fun EventAnnotationsSummaryEntity.asDomain(): EventAnnotationsSummary { + return EventAnnotationsSummaryMapper.map(this) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index 64139772..fb163dae 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -19,14 +19,17 @@ package im.vector.matrix.android.internal.database.mapper import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.UnsignedData import im.vector.matrix.android.internal.database.model.EventEntity - +import im.vector.matrix.android.internal.di.MoshiProvider +import java.util.* internal object EventMapper { fun map(event: Event, roomId: String): EventEntity { + val uds = if (event.unsignedData == null) null + else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData) val eventEntity = EventEntity() - eventEntity.eventId = event.eventId ?: "" + eventEntity.eventId = event.eventId ?: UUID.randomUUID().toString() eventEntity.roomId = event.roomId ?: roomId eventEntity.content = ContentMapper.map(event.content) val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent @@ -37,10 +40,14 @@ internal object EventMapper { eventEntity.originServerTs = event.originServerTs eventEntity.redacts = event.redacts eventEntity.age = event.unsignedData?.age ?: event.originServerTs + eventEntity.unsignedData = uds return eventEntity } fun map(eventEntity: EventEntity): Event { + //TODO proxy the event to only parse unsigned data when accessed? + var ud = if (eventEntity.unsignedData.isNullOrBlank()) null + else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(eventEntity.unsignedData) return Event( type = eventEntity.type, eventId = eventEntity.eventId, @@ -50,7 +57,7 @@ internal object EventMapper { sender = eventEntity.sender, stateKey = eventEntity.stateKey, roomId = eventEntity.roomId, - unsignedData = UnsignedData(eventEntity.age), + unsignedData = ud, redacts = eventEntity.redacts ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 9243d1de..03035128 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -37,7 +37,8 @@ internal object RoomSummaryMapper { otherMemberIds = roomSummaryEntity.otherMemberIds.toList(), highlightCount = roomSummaryEntity.highlightCount, notificationCount = roomSummaryEntity.notificationCount, - tags = tags + tags = tags, + membership = roomSummaryEntity.membership ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/UserMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/UserMapper.kt new file mode 100644 index 00000000..2389427c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/UserMapper.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.database.mapper + +import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.database.model.UserEntity + +internal object UserMapper { + + fun map(userEntity: UserEntity): User { + return User( + userEntity.userId, + userEntity.displayName, + userEntity.avatarUrl + ) + } +} + +internal fun UserEntity.asDomain(): User { + return UserMapper.map(this) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EditAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EditAggregatedSummaryEntity.kt new file mode 100644 index 00000000..f2690c96 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EditAggregatedSummaryEntity.kt @@ -0,0 +1,34 @@ +/* + * 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.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Keep the latest state of edition of a message + */ +internal open class EditAggregatedSummaryEntity( + var aggregatedContent: String? = null, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList = RealmList(), + var sourceLocalEchoEvents: RealmList = RealmList(), + var lastEditTs: Long = 0 +) : RealmObject() { + + companion object + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt new file mode 100644 index 00000000..4c6b26f0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + + +internal open class EventAnnotationsSummaryEntity( + @PrimaryKey + var eventId: String = "", + var roomId: String? = null, + var reactionsSummary: RealmList = RealmList(), + var editSummary: EditAggregatedSummaryEntity? = null +) : RealmObject() { + + companion object + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index c2fa36c1..38f4f451 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -36,6 +36,7 @@ internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUI var originServerTs: Long? = null, @Index var sender: String? = null, var age: Long? = 0, + var unsignedData: String? = null, var redacts: String? = null, @Index var stateIndex: Int = 0, @Index var displayIndex: Int = 0, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt index 3c35bf7d..35733d5f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/GroupEntity.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.database.model -import im.vector.matrix.android.api.session.room.model.MyMembership +import im.vector.matrix.android.api.session.room.model.Membership import io.realm.RealmObject import io.realm.annotations.Ignore import io.realm.annotations.PrimaryKey @@ -26,10 +26,10 @@ internal open class GroupEntity(@PrimaryKey var groupId: String = "" ) : RealmObject() { - private var membershipStr: String = MyMembership.NONE.name + private var membershipStr: String = Membership.NONE.name @delegate:Ignore - var membership: MyMembership by Delegates.observable(MyMembership.valueOf(membershipStr)) { _, _, newValue -> + var membership: Membership by Delegates.observable(Membership.valueOf(membershipStr)) { _, _, newValue -> membershipStr = newValue.name } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt new file mode 100644 index 00000000..4b94313d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/ReactionAggregatedSummaryEntity.kt @@ -0,0 +1,26 @@ +package im.vector.matrix.android.internal.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Aggregated Summary of a reaction. + */ +internal open class ReactionAggregatedSummaryEntity( + // The reaction String ๐Ÿ˜€ + var key: String = "", + // Number of time this reaction was selected + var count: Int = 0, + // Did the current user sent this reaction + var addedByMe: Boolean = false, + // The first time this reaction was added (for ordering purpose) + var firstTimestamp: Long = 0, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList = RealmList(), + // List of transaction ids for local echos + var sourceLocalEcho: RealmList = RealmList() +) : RealmObject() { + + companion object + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt index 6c61d0de..2341e492 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomEntity.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.database.model -import im.vector.matrix.android.api.session.room.model.MyMembership +import im.vector.matrix.android.api.session.room.model.Membership import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.Ignore @@ -30,10 +30,10 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "", var areAllMembersLoaded: Boolean = false ) : RealmObject() { - private var membershipStr: String = MyMembership.NONE.name + private var membershipStr: String = Membership.NONE.name @delegate:Ignore - var membership: MyMembership by Delegates.observable(MyMembership.valueOf(membershipStr)) { _, _, newValue -> + var membership: Membership by Delegates.observable(Membership.valueOf(membershipStr)) { _, _, newValue -> membershipStr = newValue.name } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 9f9c66f0..32f27adb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -16,9 +16,12 @@ package im.vector.matrix.android.internal.database.model +import im.vector.matrix.android.api.session.room.model.Membership import io.realm.RealmList import io.realm.RealmObject +import io.realm.annotations.Ignore import io.realm.annotations.PrimaryKey +import kotlin.properties.Delegates internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var displayName: String? = "", @@ -35,6 +38,13 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", var tags: RealmList = RealmList() ) : RealmObject() { + private var membershipStr: String = Membership.NONE.name + + @delegate:Ignore + var membership: Membership by Delegates.observable(Membership.valueOf(membershipStr)) { _, _, newValue -> + membershipStr = newValue.name + } + companion object } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index a5042634..20ba8d44 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -33,6 +33,9 @@ import io.realm.annotations.RealmModule RoomSummaryEntity::class, RoomTagEntity::class, SyncEntity::class, - UserEntity::class + UserEntity::class, + EventAnnotationsSummaryEntity::class, + ReactionAggregatedSummaryEntity::class, + EditAggregatedSummaryEntity::class ]) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt new file mode 100644 index 00000000..b738af35 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventAnnotationsSummaryEntityQuery.kt @@ -0,0 +1,27 @@ +package im.vector.matrix.android.internal.database.query + +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntityFields +import io.realm.Realm +import io.realm.RealmQuery +import io.realm.kotlin.where + +internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + val query = realm.where() + query.equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId) + return query +} + +internal fun EventAnnotationsSummaryEntity.Companion.whereInRoom(realm: Realm, roomId: String?): RealmQuery { + val query = realm.where() + if (roomId != null) { + query.equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId) + } + return query +} + + +internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, eventId: String): EventAnnotationsSummaryEntity { + val obj = realm.createObject(EventAnnotationsSummaryEntity::class.java, eventId) + return obj +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index bf8d59c4..8fca0abd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -52,6 +52,7 @@ internal fun EventEntity.Companion.where(realm: Realm, } } + internal fun EventEntity.Companion.latestEvent(realm: Realm, roomId: String, includedTypes: List = emptyList(), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupEntityQueries.kt index 692ecc1d..3d27a56b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/GroupEntityQueries.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.database.query -import im.vector.matrix.android.api.session.room.model.MyMembership +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.model.GroupEntityFields import io.realm.Realm @@ -27,7 +27,7 @@ internal fun GroupEntity.Companion.where(realm: Realm, roomId: String): RealmQue return realm.where().equalTo(GroupEntityFields.GROUP_ID, roomId) } -internal fun GroupEntity.Companion.where(realm: Realm, membership: MyMembership? = null): RealmQuery { +internal fun GroupEntity.Companion.where(realm: Realm, membership: Membership? = null): RealmQuery { val query = realm.where() if (membership != null) { query.equalTo(GroupEntityFields.MEMBERSHIP_STR, membership.name) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomEntityQueries.kt index 211421ef..031cb93e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomEntityQueries.kt @@ -16,7 +16,7 @@ package im.vector.matrix.android.internal.database.query -import im.vector.matrix.android.api.session.room.model.MyMembership +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntityFields @@ -28,7 +28,7 @@ internal fun RoomEntity.Companion.where(realm: Realm, roomId: String): RealmQuer return realm.where().equalTo(RoomEntityFields.ROOM_ID, roomId) } -internal fun RoomEntity.Companion.where(realm: Realm, membership: MyMembership? = null): RealmQuery { +internal fun RoomEntity.Companion.where(realm: Realm, membership: Membership? = null): RealmQuery { val query = realm.where() if (membership != null) { query.equalTo(RoomEntityFields.MEMBERSHIP_STR, membership.name) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt index 1400fc38..8f9447c6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixModule.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import im.vector.matrix.android.internal.util.StringProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.android.asCoroutineDispatcher import org.koin.dsl.module.module @@ -46,6 +47,9 @@ class MatrixModule(private val context: Context) { single { TaskExecutor(get()) } + single { + StringProvider(context.resources) + } single { BackgroundDetectionObserver() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 8ca4c8ea..12e52576 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -37,13 +37,18 @@ import im.vector.matrix.android.api.session.group.Group import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.room.Room +import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams +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.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.MatrixCallbackDelegate import im.vector.matrix.android.internal.crypto.CryptoManager import im.vector.matrix.android.internal.crypto.CryptoModule @@ -78,6 +83,7 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi private val liveEntityUpdaters by inject>() private val sessionListeners by inject() private val roomService by inject() + private val roomDirectoryService by inject() private val groupService by inject() private val userService by inject() private val filterService by inject() @@ -195,6 +201,23 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi return roomService.liveRoomSummaries() } + // ROOM DIRECTORY SERVICE + + override fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback): Cancelable { + assert(isOpen) + return roomDirectoryService.getPublicRooms(server, publicRoomsParams, callback) + } + + override fun joinRoom(roomId: String, callback: MatrixCallback) { + assert(isOpen) + return roomDirectoryService.joinRoom(roomId, callback) + } + + override fun getThirdPartyProtocol(callback: MatrixCallback>) { + assert(isOpen) + return roomDirectoryService.getThirdPartyProtocol(callback) + } + // GROUP SERVICE override fun getGroup(groupId: String): Group? { @@ -232,6 +255,11 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi return userService.getUser(userId) } + override fun observeUser(userId: String): LiveData { + assert(isOpen) + return userService.observeUser(userId) + } + // CRYPTO SERVICE override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index d877bd3f..f46ab758 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.group.GroupService +import im.vector.matrix.android.api.session.room.RoomDirectoryService import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.sync.FilterService @@ -33,11 +34,13 @@ import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask import im.vector.matrix.android.internal.session.filter.* import im.vector.matrix.android.internal.session.group.DefaultGroupService import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater -import im.vector.matrix.android.internal.session.room.DefaultRoomService -import im.vector.matrix.android.internal.session.room.RoomAvatarResolver -import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater -import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver -import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver +import im.vector.matrix.android.internal.session.room.* +import im.vector.matrix.android.internal.session.room.directory.DefaultGetPublicRoomTask +import im.vector.matrix.android.internal.session.room.directory.DefaultGetThirdPartyProtocolsTask +import im.vector.matrix.android.internal.session.room.directory.GetPublicRoomTask +import im.vector.matrix.android.internal.session.room.directory.GetThirdPartyProtocolsTask +import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver +import im.vector.matrix.android.internal.session.room.membership.RoomMemberDisplayNameResolver import im.vector.matrix.android.internal.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.signout.DefaultSignOutService import im.vector.matrix.android.internal.session.user.DefaultUserService @@ -106,6 +109,18 @@ internal class SessionModule(private val sessionParams: SessionParams) { DefaultRoomService(get(), get(), get(), get()) as RoomService } + scope(DefaultSession.SCOPE) { + DefaultGetPublicRoomTask(get()) as GetPublicRoomTask + } + + scope(DefaultSession.SCOPE) { + DefaultGetThirdPartyProtocolsTask(get()) as GetThirdPartyProtocolsTask + } + + scope(DefaultSession.SCOPE) { + DefaultRoomDirectoryService(get(), get(), get(), get()) as RoomDirectoryService + } + scope(DefaultSession.SCOPE) { DefaultGroupService(get()) as GroupService } @@ -149,9 +164,11 @@ internal class SessionModule(private val sessionParams: SessionParams) { scope(DefaultSession.SCOPE) { val groupSummaryUpdater = GroupSummaryUpdater(get()) - val eventsPruner = EventsPruner(get()) val userEntityUpdater = UserEntityUpdater(get(), get(), get()) - listOf(groupSummaryUpdater, eventsPruner, userEntityUpdater) + val aggregationUpdater = EventRelationsAggregationUpdater(get(), get(), get(), get()) + //Event pruner must be the last one, because it will clear contents + val eventsPruner = EventsPruner(get(), get(), get(), get()) + listOf(groupSummaryUpdater, userEntityUpdater, aggregationUpdater, eventsPruner) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt index cb8896e7..0fad40d0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUrlResolver.kt @@ -21,10 +21,19 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver private const val MATRIX_CONTENT_URI_SCHEME = "mxc://" -internal const val URI_PREFIX_CONTENT_API = "/_matrix/media/v1/" +private const val URI_PREFIX_CONTENT_API = "_matrix/media/v1/" internal class DefaultContentUrlResolver(private val homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver { + companion object { + fun getUploadUrl(homeServerConnectionConfig: HomeServerConnectionConfig): String { + val baseUrl = homeServerConnectionConfig.homeServerUri.toString() + val sep = if (baseUrl.endsWith("/")) "" else "/" + + return baseUrl + sep + URI_PREFIX_CONTENT_API + "upload" + } + } + override fun resolveFullSize(contentUrl: String?): String? { if (contentUrl?.isValidMatrixContentUrl() == true) { val baseUrl = homeServerConnectionConfig.homeServerUri.toString() @@ -57,7 +66,10 @@ internal class DefaultContentUrlResolver(private val homeServerConnectionConfig: fragment = serverAndMediaId.substring(fragmentOffset) serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset) } - return baseUrl + prefix + serverAndMediaId + (params ?: "") + fragment + + val sep = if (baseUrl.endsWith("/")) "" else "/" + + return baseUrl + sep + prefix + serverAndMediaId + (params ?: "") + fragment } private fun String.isValidMatrixContentUrl(): Boolean { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 1ec18127..e8133070 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -21,19 +21,15 @@ import arrow.core.Try.Companion.raise import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.network.ProgressRequestBody -import okhttp3.HttpUrl -import okhttp3.MediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody +import okhttp3.* import java.io.File import java.io.IOException internal class FileUploader(private val okHttpClient: OkHttpClient, - private val sessionParams: SessionParams) { + sessionParams: SessionParams) { - private val uploadUrl = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload" + private val uploadUrl = DefaultContentUrlResolver.getUploadUrl(sessionParams.homeServerConnectionConfig) private val moshi = MoshiProvider.providesMoshi() private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) @@ -82,7 +78,7 @@ internal class FileUploader(private val okHttpClient: OkHttpClient, response.body()?.source()?.let { responseAdapter.fromJson(it) } - ?: throw IOException() + ?: throw IOException() } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 1a4202b6..5c37fa7d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -21,8 +21,9 @@ import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.Room -import im.vector.matrix.android.api.session.room.members.RoomMembersService +import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService @@ -40,14 +41,16 @@ internal class DefaultRoom( private val sendService: SendService, private val stateService: StateService, private val readService: ReadService, - private val roomMembersService: RoomMembersService, - private val cryptoService: CryptoService + private val cryptoService: CryptoService, + private val relationService: RelationService, + private val roomMembersService: MembershipService ) : Room, TimelineService by timelineService, SendService by sendService, StateService by stateService, ReadService by readService, - RoomMembersService by roomMembersService { + RelationService by relationService, + MembershipService by roomMembersService { override val roomSummary: LiveData by lazy { val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt new file mode 100644 index 00000000..1a434dd6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomDirectoryService.kt @@ -0,0 +1,58 @@ +/* + * 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 + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.room.RoomDirectoryService +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.api.util.Cancelable +import im.vector.matrix.android.internal.session.room.directory.GetPublicRoomTask +import im.vector.matrix.android.internal.session.room.directory.GetThirdPartyProtocolsTask +import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith + +internal class DefaultRoomDirectoryService(private val getPublicRoomTask: GetPublicRoomTask, + private val joinRoomTask: JoinRoomTask, + private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask, + private val taskExecutor: TaskExecutor) : RoomDirectoryService { + + override fun getPublicRooms(server: String?, + publicRoomsParams: PublicRoomsParams, + callback: MatrixCallback): Cancelable { + return getPublicRoomTask + .configureWith(GetPublicRoomTask.Params(server, publicRoomsParams)) + .dispatchTo(callback) + .executeBy(taskExecutor) + } + + override fun joinRoom(roomId: String, callback: MatrixCallback) { + joinRoomTask + .configureWith(JoinRoomTask.Params(roomId)) + .dispatchTo(callback) + .executeBy(taskExecutor) + } + + override fun getThirdPartyProtocol(callback: MatrixCallback>) { + getThirdPartyProtocolsTask + .configureWith(Unit) + .dispatchTo(callback) + .executeBy(taskExecutor) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationExtractor.kt new file mode 100644 index 00000000..e52f5e09 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationExtractor.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.room + +import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.query.where +import io.realm.Realm + +/** + * Fetches annotations (reactions, edits...) associated to a given eventEntity from the data layer. + */ +internal class EventRelationExtractor { + + fun extractFrom(event: EventEntity, realm: Realm = event.realm): EventAnnotationsSummary? { + return EventAnnotationsSummaryEntity.where(realm, event.eventId).findFirst()?.asDomain() + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt new file mode 100644 index 00000000..275c4d02 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -0,0 +1,309 @@ +/* + * 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 + +import arrow.core.Try +import com.zhuinden.monarchy.Monarchy +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.api.session.room.send.SendState +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.* +import im.vector.matrix.android.internal.database.query.create +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.tryTransactionAsync +import io.realm.Realm +import timber.log.Timber + +internal interface EventRelationsAggregationTask : Task { + + data class Params( + val events: List>, + val userId: String + ) +} + +/** + * Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base. + */ +internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarchy) : EventRelationsAggregationTask { + + override suspend fun execute(params: EventRelationsAggregationTask.Params): Try { + return monarchy.tryTransactionAsync { realm -> + update(realm, params.events, params.userId) + } + } + + private fun update(realm: Realm, events: List>, userId: String) { + events.forEach { pair -> + val roomId = pair.first.roomId ?: return@forEach + val event = pair.first + val sendState = pair.second + val isLocalEcho = sendState == SendState.UNSENT + when (event.type) { + EventType.REACTION -> { + //we got a reaction!! + Timber.v("###REACTION in room $roomId") + handleReaction(event, roomId, realm, userId, isLocalEcho) + } + EventType.MESSAGE -> { + if (event.unsignedData?.relations?.annotations != null) { + Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") + handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) + } else { + val content: MessageContent? = event.content.toModel() + if (content?.relatesTo?.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + //A replace! + handleReplace(realm, event, content, roomId, isLocalEcho) + } + } + + } + EventType.REDACTION -> { + val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } + ?: return + when (eventToPrune.type) { + EventType.MESSAGE -> { + Timber.d("REDACTION for message ${eventToPrune.eventId}") + val unsignedData = EventMapper.map(eventToPrune).unsignedData + ?: UnsignedData(null, null) + + //was this event a m.replace + val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() + if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { + handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) + } + + } + EventType.REACTION -> { + handleReactionRedact(eventToPrune, realm, userId) + } + } + } + } + } + } + + private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean) { + val eventId = event.eventId ?: return + val targetEventId = content.relatesTo?.eventId ?: return + val newContent = content.newContent ?: return + //ok, this is a replace + var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() + if (existing == null) { + Timber.v("###REPLACE creating no relation summary for ${targetEventId}") + existing = EventAnnotationsSummaryEntity.create(realm, targetEventId) + existing.roomId = roomId + } + + //we have it + val existingSummary = existing.editSummary + if (existingSummary == null) { + Timber.v("###REPLACE no edit summary for ${targetEventId}, creating one (localEcho:$isLocalEcho)") + //create the edit summary + val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java) + editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() + editSummary.aggregatedContent = ContentMapper.map(newContent) + if (isLocalEcho) { + editSummary.sourceLocalEchoEvents.add(eventId) + } else { + editSummary.sourceEvents.add(eventId) + } + + existing.editSummary = editSummary + } else { + if (existingSummary.sourceEvents.contains(eventId)) { + //ignore this event, we already know it (??) + Timber.v("###REPLACE ignoring event for summary, it's known ${eventId}") + return + } + val txId = event.unsignedData?.transactionId + //is it a remote echo? + if (!isLocalEcho && existingSummary.sourceLocalEchoEvents.contains(txId)) { + //ok it has already been managed + Timber.v("###REPLACE Receiving remote echo of edit (edit already done)") + existingSummary.sourceLocalEchoEvents.remove(txId) + existingSummary.sourceEvents.add(event.eventId) + } else if (event.originServerTs ?: 0 > existingSummary.lastEditTs) { + Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)") + existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis() + existingSummary.aggregatedContent = ContentMapper.map(newContent) + existingSummary.sourceEvents.add(eventId) + } else { + //ignore this event for the summary + Timber.v("###REPLACE ignoring event for summary, it's to old ${eventId}") + } + } + + } + + private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) { + aggregation.chunk?.forEach { + if (it.type == EventType.REACTION) { + val eventId = event.eventId ?: "" + val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + if (existing == null) { + val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId) + eventSummary.roomId = roomId + val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = it.key + sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order? + sum.count = it.count + eventSummary.reactionsSummary.add(sum) + } else { + //TODO how to handle that + } + } + } + } + + private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) { + event.content.toModel()?.let { content -> + //rel_type must be m.annotation + if (RelationType.ANNOTATION == content.relatesTo?.type) { + val reaction = content.relatesTo.key + val eventId = content.relatesTo.eventId + val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + ?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId } + + var sum = eventSummary.reactionsSummary.find { it.key == reaction } + val txId = event.unsignedData?.transactionId + if (isLocalEcho && txId.isNullOrBlank()) { + Timber.w("Received a local echo with no transaction ID") + } + if (sum == null) { + sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) + sum.key = reaction + sum.firstTimestamp = event.originServerTs ?: 0 + if (isLocalEcho) { + Timber.v("Adding local echo reaction $reaction") + sum.sourceLocalEcho.add(txId) + sum.count = 1 + } else { + Timber.v("Adding synced reaction $reaction") + sum.count = 1 + sum.sourceEvents.add(event.eventId) + } + sum.addedByMe = sum.addedByMe || (userId == event.sender) + eventSummary.reactionsSummary.add(sum) + } else { + //is this a known event (is possible? pagination?) + if (!sum.sourceEvents.contains(eventId)) { + + //check if it's not the sync of a local echo + if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) { + //ok it has already been counted, just sync the list, do not touch count + Timber.v("Ignoring synced of local echo for reaction $reaction") + sum.sourceLocalEcho.remove(txId) + sum.sourceEvents.add(event.eventId) + } else { + sum.count += 1 + if (isLocalEcho) { + Timber.v("Adding local echo reaction $reaction") + sum.sourceLocalEcho.add(txId) + } else { + Timber.v("Adding synced reaction $reaction") + sum.sourceEvents.add(event.eventId) + } + + sum.addedByMe = sum.addedByMe || (userId == event.sender) + } + + } + } + + } + } + } + + /** + * Called when an event is deleted + */ + private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) { + Timber.d("Handle redaction of m.replace") + val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst() + if (eventSummary == null) { + Timber.w("Redaction of a replace targeting an unknown event $relatedEventId") + return + } + val sourceEvents = eventSummary.editSummary?.sourceEvents + val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId) + if (sourceToDiscard == null) { + Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard") + return + } + //Need to remove this event from the redaction list and compute new aggregation state + sourceEvents.removeAt(sourceToDiscard) + val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull() + if (previousEdit == null) { + //revert to original + eventSummary.editSummary?.deleteFromRealm() + } else { + //I have the last event + ContentMapper.map(previousEdit.content)?.toModel()?.newContent?.let { newContent -> + eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs + ?: System.currentTimeMillis() + eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent) + } ?: run { + Timber.e("Failed to udate edited summary") + //TODO how to reccover that + } + + } + } + + fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) { + Timber.v("REDACTION of reaction ${eventToPrune.eventId}") + //delete a reaction, need to update the annotation summary if any + val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() + ?: return + val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return + + val reactionKey = reactionContent.relatesTo.key + Timber.v("REMOVE reaction for key $reactionKey") + val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst() + if (summary != null) { + summary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionKey) + .findFirst()?.let { aggregation -> + Timber.v("Find summary for key with ${aggregation.sourceEvents.size} known reactions (count:${aggregation.count})") + Timber.v("Known reactions ${aggregation.sourceEvents.joinToString(",")}") + if (aggregation.sourceEvents.contains(eventToPrune.eventId)) { + Timber.v("REMOVE reaction for key $reactionKey") + aggregation.sourceEvents.remove(eventToPrune.eventId) + Timber.v("Known reactions after ${aggregation.sourceEvents.joinToString(",")}") + aggregation.count = aggregation.count - 1 + if (eventToPrune.sender == userId) { + //Was it a redact on my reaction? + aggregation.addedByMe = false + } + if (aggregation.count == 0) { + //delete! + aggregation.deleteFromRealm() + } + } else { + Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known") + } + } + } else { + Timber.e("## Cannot find summary for key $reactionKey") + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt new file mode 100644 index 00000000..0b29064c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -0,0 +1,63 @@ +/* + * 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 + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.database.RealmLiveEntityObserver +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import timber.log.Timber + +/** + * Acts as a listener of incoming messages in order to incrementally computes a summary of annotations. + * For reactions will build a EventAnnotationsSummaryEntity, ans for edits a EditAggregatedSummaryEntity. + * The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display. + */ +internal class EventRelationsAggregationUpdater(monarchy: Monarchy, + private val credentials: Credentials, + private val task: EventRelationsAggregationTask, + private val taskExecutor: TaskExecutor) : + RealmLiveEntityObserver(monarchy) { + + override val query = Monarchy.Query { + EventEntity.where(it) + //mmm why is this query not working? +// EventEntity.byTypes(it, listOf( +// EventType.REDACTION, EventType.MESSAGE, EventType.REDACTION) +// ) + } + + override fun processChanges(inserted: List, updated: List, deleted: List) { + Timber.v("EventRelationsAggregationUpdater called with ${inserted.size} insertions") + val inserted = inserted + .mapNotNull { it.asDomain() to it.sendState } + + val params = EventRelationsAggregationTask.Params( + inserted, + credentials.userId + ) + + task.configureWith(params) + .executeBy(taskExecutor) + + } + +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index c3617504..af263970 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -20,9 +20,12 @@ import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event 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.internal.network.NetworkConstants -import im.vector.matrix.android.internal.session.room.invite.InviteBody -import im.vector.matrix.android.internal.session.room.members.RoomMembersResponse +import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse +import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody +import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol 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 @@ -31,6 +34,25 @@ import retrofit2.http.* internal interface RoomAPI { + /** + * Get the third party server protocols. + * + * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-thirdparty-protocols + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols") + fun thirdPartyProtocols(): Call> + + /** + * Lists the public rooms on the server, with optional filter. + * This API returns paginated responses. The rooms are ordered by the number of joined members, with the largest rooms first. + * + * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-publicrooms + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "publicRooms") + fun publicRooms(@Query("server") server: String?, + @Body publicRoomsParams: PublicRoomsParams + ): Call + /** * Create a room. * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-createroom @@ -156,4 +178,58 @@ internal interface RoomAPI { @Path("state_event_type") stateEventType: String, @Path("state_key") stateKey: String, @Body params: Map): Call + + /** + * Send a relation event to a room. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventType the event type + * @param content the event content + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}") + fun sendRelation(@Path("roomId") roomId: String, + @Path("parentId") parent_id: String, + @Path("relation_type") relationType: String, + @Path("eventType") eventType: String, + @Body content: Content? + ): Call + + /** + * Join the given room. + * + * @param roomId the room id + * @param params the request body + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/join") + fun join(@Path("roomId") roomId: String, + @Body params: Map): Call + + /** + * Leave the given room. + * + * @param roomId the room id + * @param params the request body + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave") + fun leave(@Path("roomId") roomId: String, + @Body params: Map): Call + + /** + * Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room. + * This cannot be undone. + * Users may redact their own events, and any user with a power level greater than or equal to the redact power level of the room may redact events there. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventId the event to delete + * @param reason json containing reason key {"reason": "Indecent material"} + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}") + fun redactEvent( + @Path("txnId") txId: String, + @Path("roomId") roomId: String, + @Path("eventId") parent_id: String, + @Body reason: Map + ): Call } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt index c6786508..c26f0596 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAvatarResolver.kt @@ -20,14 +20,14 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.MyMembership +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomAvatarContent import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.room.members.RoomMembers +import im.vector.matrix.android.internal.session.room.membership.RoomMembers internal class RoomAvatarResolver(private val monarchy: Monarchy, private val credentials: Credentials) { @@ -48,7 +48,7 @@ internal class RoomAvatarResolver(private val monarchy: Monarchy, } val roomMembers = RoomMembers(realm, roomId) val members = roomMembers.getLoaded() - if (roomEntity?.membership == MyMembership.INVITED) { + if (roomEntity?.membership == Membership.INVITE) { if (members.size == 1) { res = members.entries.first().value.avatarUrl } else if (members.size > 1) { @@ -57,9 +57,9 @@ internal class RoomAvatarResolver(private val monarchy: Monarchy, } } else { // detect if it is a room with no more than 2 members (i.e. an alone or a 1:1 chat) - if (roomMembers.getNumberOfJoinedMembers() == 1 && members.isNotEmpty()) { + if (members.size == 1) { res = members.entries.first().value.avatarUrl - } else if (roomMembers.getNumberOfMembers() == 2 && members.size > 1) { + } else if (members.size == 2) { val firstOtherMember = members.filterKeys { it != credentials.userId }.values.firstOrNull() res = firstOtherMember?.avatarUrl } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index eb285150..4f4b7e73 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -19,13 +19,17 @@ package im.vector.matrix.android.internal.session.room import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.room.Room -import im.vector.matrix.android.internal.crypto.CryptoManager -import im.vector.matrix.android.internal.session.room.invite.InviteTask -import im.vector.matrix.android.internal.session.room.members.DefaultRoomMembersService -import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask -import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor +import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService +import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask +import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor +import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask +import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask +import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask 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.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 @@ -36,25 +40,31 @@ import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory import im.vector.matrix.android.internal.task.TaskExecutor -internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, +internal class RoomFactory(private val monarchy: Monarchy, + private val eventFactory: LocalEchoEventFactory, + private val taskExecutor: TaskExecutor, + private val loadRoomMembersTask: LoadRoomMembersTask, private val inviteTask: InviteTask, private val sendStateTask: SendStateTask, - private val monarchy: Monarchy, private val paginationTask: PaginationTask, private val contextOfEventTask: GetContextOfEventTask, private val setReadMarkersTask: SetReadMarkersTask, - private val eventFactory: LocalEchoEventFactory, private val cryptoService: CryptoService, - private val taskExecutor: TaskExecutor) { + private val findReactionEventForUndoTask: FindReactionEventForUndoTask, + private val updateQuickReactionTask: UpdateQuickReactionTask, + private val joinRoomTask: JoinRoomTask, + private val leaveRoomTask: LeaveRoomTask) { fun instantiate(roomId: String): Room { val roomMemberExtractor = SenderRoomMemberExtractor(roomId) - val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, cryptoService) - val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask) + val relationExtractor = EventRelationExtractor() + val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, relationExtractor, cryptoService) + val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineEventFactory, contextOfEventTask, paginationTask) val sendService = DefaultSendService(roomId, eventFactory, cryptoService, monarchy) - val stateService = DefaultStateService(roomId, sendStateTask, taskExecutor) - val roomMembersService = DefaultRoomMembersService(roomId, monarchy, loadRoomMembersTask, inviteTask, taskExecutor) - val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor) + val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask) + val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) + val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask) + val reactionService = DefaultRelationService(roomId, eventFactory, findReactionEventForUndoTask, updateQuickReactionTask, monarchy, taskExecutor) return DefaultRoom( roomId, @@ -63,8 +73,9 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, sendService, stateService, readService, - roomMembersService, - cryptoService + cryptoService, + reactionService, + roomMembersService ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index e48d25e2..0ae5bd0e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -19,20 +19,26 @@ package im.vector.matrix.android.internal.session.room import im.vector.matrix.android.internal.session.DefaultSession import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask -import im.vector.matrix.android.internal.session.room.invite.DefaultInviteTask -import im.vector.matrix.android.internal.session.room.invite.InviteTask -import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask -import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask +import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask +import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask +import im.vector.matrix.android.internal.session.room.membership.joining.DefaultInviteTask +import im.vector.matrix.android.internal.session.room.membership.joining.DefaultJoinRoomTask +import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask +import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask +import im.vector.matrix.android.internal.session.room.membership.leaving.DefaultLeaveRoomTask +import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask +import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask +import im.vector.matrix.android.internal.session.room.prune.PruneEventTask import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask +import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask +import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask +import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask +import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask -import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask -import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask -import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask -import im.vector.matrix.android.internal.session.room.timeline.PaginationTask -import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor +import im.vector.matrix.android.internal.session.room.timeline.* import org.koin.dsl.module.module import retrofit2.Retrofit @@ -67,11 +73,11 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - LocalEchoEventFactory(get()) + LocalEchoEventFactory(get(), get()) } scope(DefaultSession.SCOPE) { - RoomFactory(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) + RoomFactory(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } scope(DefaultSession.SCOPE) { @@ -82,9 +88,33 @@ class RoomModule { DefaultInviteTask(get()) as InviteTask } + scope(DefaultSession.SCOPE) { + DefaultJoinRoomTask(get()) as JoinRoomTask + } + + scope(DefaultSession.SCOPE) { + DefaultLeaveRoomTask(get()) as LeaveRoomTask + } + scope(DefaultSession.SCOPE) { DefaultSendStateTask(get()) as SendStateTask } + scope(DefaultSession.SCOPE) { + DefaultFindReactionEventForUndoTask(get()) as FindReactionEventForUndoTask + } + + scope(DefaultSession.SCOPE) { + DefaultUpdateQuickReactionTask(get()) as UpdateQuickReactionTask + } + + scope(DefaultSession.SCOPE) { + DefaultPruneEventTask(get()) as PruneEventTask + } + + scope(DefaultSession.SCOPE) { + DefaultEventRelationsAggregationTask(get()) as EventRelationsAggregationTask + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index c8ed0ee9..7bf08b70 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -21,6 +21,7 @@ package im.vector.matrix.android.internal.session.room import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity @@ -28,8 +29,8 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver -import im.vector.matrix.android.internal.session.room.members.RoomMembers +import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver +import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications import io.realm.Realm @@ -41,11 +42,12 @@ internal class RoomSummaryUpdater(private val credentials: Credentials, fun update(realm: Realm, roomId: String, + membership: Membership? = null, roomSummary: RoomSyncSummary? = null, unreadNotifications: RoomSyncUnreadNotifications? = null) { val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) if (roomSummary != null) { if (roomSummary.heroes.isNotEmpty()) { @@ -65,8 +67,11 @@ internal class RoomSummaryUpdater(private val credentials: Credentials, if (unreadNotifications?.notificationCount != null) { roomSummaryEntity.notificationCount = unreadNotifications.notificationCount } + if (membership != null) { + roomSummaryEntity.membership = membership + } - val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = listOf(EventType.MESSAGE)) + val lastEvent = EventEntity.latestEvent(realm, roomId) val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain() val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId } roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetPublicRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetPublicRoomTask.kt new file mode 100644 index 00000000..4b71b871 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetPublicRoomTask.kt @@ -0,0 +1,40 @@ +/* + * 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.directory + +import arrow.core.Try +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.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task + +internal interface GetPublicRoomTask : Task { + data class Params( + val server: String?, + val publicRoomsParams: PublicRoomsParams + ) +} + +internal class DefaultGetPublicRoomTask(private val roomAPI: RoomAPI) : GetPublicRoomTask { + + override suspend fun execute(params: GetPublicRoomTask.Params): Try { + return executeRequest { + apiCall = roomAPI.publicRooms(params.server, params.publicRoomsParams) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetThirdPartyProtocolsTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetThirdPartyProtocolsTask.kt new file mode 100644 index 00000000..a50ec367 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/directory/GetThirdPartyProtocolsTask.kt @@ -0,0 +1,34 @@ +/* + * 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.directory + +import arrow.core.Try +import im.vector.matrix.android.api.session.room.model.thirdparty.ThirdPartyProtocol +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 + +internal interface GetThirdPartyProtocolsTask : Task> + +internal class DefaultGetThirdPartyProtocolsTask(private val roomAPI: RoomAPI) : GetThirdPartyProtocolsTask { + + override suspend fun execute(params: Unit): Try> { + return executeRequest { + apiCall = roomAPI.thirdPartyProtocols() + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt similarity index 61% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt index a007a1eb..a9b48ea6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/DefaultMembershipService.kt @@ -16,28 +16,32 @@ * */ -package im.vector.matrix.android.internal.session.room.members +package im.vector.matrix.android.internal.session.room.membership import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.members.RoomMembersService +import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.mapper.asDomain -import im.vector.matrix.android.internal.session.room.invite.InviteTask +import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask +import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask +import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.fetchCopied -internal class DefaultRoomMembersService(private val roomId: String, - private val monarchy: Monarchy, - private val loadRoomMembersTask: LoadRoomMembersTask, - private val inviteTask: InviteTask, - private val taskExecutor: TaskExecutor -) : RoomMembersService { +internal class DefaultMembershipService(private val roomId: String, + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, + private val loadRoomMembersTask: LoadRoomMembersTask, + private val inviteTask: InviteTask, + private val joinTask: JoinRoomTask, + private val leaveRoomTask: LeaveRoomTask +) : MembershipService { override fun loadRoomMembersIfNeeded(): Cancelable { val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE) @@ -68,4 +72,18 @@ internal class DefaultRoomMembersService(private val roomId: String, .dispatchTo(callback) .executeBy(taskExecutor) } + + override fun join(callback: MatrixCallback) { + val params = JoinRoomTask.Params(roomId) + joinTask.configureWith(params) + .dispatchTo(callback) + .executeBy(taskExecutor) + } + + override fun leave(callback: MatrixCallback) { + val params = LeaveRoomTask.Params(roomId) + leaveRoomTask.configureWith(params) + .dispatchTo(callback) + .executeBy(taskExecutor) + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt similarity index 98% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt index 425f9fc1..44a050db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/LoadRoomMembersTask.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.members +package im.vector.matrix.android.internal.session.room.membership import arrow.core.Try import com.zhuinden.monarchy.Monarchy diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt similarity index 85% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt index 7f19762c..605de81a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.members +package im.vector.matrix.android.internal.session.room.membership import android.content.Context import com.zhuinden.monarchy.Monarchy @@ -22,7 +22,7 @@ import im.vector.matrix.android.R import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.MyMembership +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomAliasesContent import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent import im.vector.matrix.android.api.session.room.model.RoomNameContent @@ -77,24 +77,17 @@ internal class RoomDisplayNameResolver(private val context: Context, } val roomMembers = RoomMembers(realm, roomId) - val otherRoomMembers = roomMembers.getLoaded() - .filterKeys { it != credentials.userId } - - if (roomEntity?.membership == MyMembership.INVITED) { - //TODO handle invited - /* - if (currentUser != null - && !othersActiveMembers.isEmpty() - && !TextUtils.isEmpty(currentUser!!.mSender)) { - // extract who invited us to the room - name = context.getString(R.string.room_displayname_invite_from, roomState.resolve(currentUser!!.mSender)) + val loadedMembers = roomMembers.getLoaded() + val otherRoomMembers = loadedMembers.filterKeys { it != credentials.userId } + if (roomEntity?.membership == Membership.INVITE) { + val inviteMeEvent = roomMembers.queryRoomMemberEvent(credentials.userId).findFirst() + val inviterId = inviteMeEvent?.sender + name = if (inviterId != null && otherRoomMembers.containsKey(inviterId)) { + roomMemberDisplayNameResolver.resolve(inviterId, otherRoomMembers) } else { - name = context.getString(R.string.room_displayname_room_invite) + context.getString(R.string.room_displayname_room_invite) } - */ - name = context.getString(R.string.room_displayname_room_invite) } else { - val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() val memberIds = if (roomSummary?.heroes?.isNotEmpty() == true) { roomSummary.heroes diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberDisplayNameResolver.kt similarity index 96% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberDisplayNameResolver.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberDisplayNameResolver.kt index fb17872d..97435bd4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMemberDisplayNameResolver.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.members +package im.vector.matrix.android.internal.session.room.membership import im.vector.matrix.android.api.session.room.model.RoomMember diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt similarity index 98% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt index a441af94..ff8b8ce4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembers.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.members +package im.vector.matrix.android.internal.session.room.membership import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembersResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembersResponse.kt similarity index 92% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembersResponse.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembersResponse.kt index 26bd759a..af3bf9b6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembersResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/RoomMembersResponse.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.members +package im.vector.matrix.android.internal.session.room.membership import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/SenderRoomMemberExtractor.kt similarity index 97% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/SenderRoomMemberExtractor.kt index de2d3de0..c8944bac 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/SenderRoomMemberExtractor.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.members +package im.vector.matrix.android.internal.session.room.membership import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteBody.kt similarity index 85% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteBody.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteBody.kt index 652f8d63..4529a17a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteBody.kt @@ -5,7 +5,7 @@ * 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 + * 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, @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.invite +package im.vector.matrix.android.internal.session.room.membership.joining import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt similarity index 94% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteTask.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt index d3a67dd2..3610cbe0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/invite/InviteTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/InviteTask.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.session.room.invite +package im.vector.matrix.android.internal.session.room.membership.joining import arrow.core.Try import im.vector.matrix.android.internal.network.executeRequest diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt new file mode 100644 index 00000000..3fe2d139 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/joining/JoinRoomTask.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.membership.joining + +import arrow.core.Try +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 + +internal interface JoinRoomTask : Task { + data class Params( + val roomId: String + ) +} + +internal class DefaultJoinRoomTask(private val roomAPI: RoomAPI) : JoinRoomTask { + + override suspend fun execute(params: JoinRoomTask.Params): Try { + return executeRequest { + apiCall = roomAPI.join(params.roomId, HashMap()) + } + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt new file mode 100644 index 00000000..9eb1728f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/membership/leaving/LeaveRoomTask.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.room.membership.leaving + +import arrow.core.Try +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 + +internal interface LeaveRoomTask : Task { + data class Params( + val roomId: String + ) +} + +internal class DefaultLeaveRoomTask(private val roomAPI: RoomAPI) : LeaveRoomTask { + + override suspend fun execute(params: LeaveRoomTask.Params): Try { + return executeRequest { + apiCall = roomAPI.leave(params.roomId, HashMap()) + } + } + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt index ad17032a..4d3bc6fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt @@ -16,38 +16,42 @@ package im.vector.matrix.android.internal.session.room.prune -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.util.WorkerParamsFactory +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import timber.log.Timber -private const val PRUNE_EVENT_WORKER = "PRUNE_EVENT_WORKER" - -internal class EventsPruner(monarchy: Monarchy) : +/** + * Listens to the database for the insertion of any redaction event. + * As it will actually delete the content, it should be called last in the list of listener. + */ +internal class EventsPruner(monarchy: Monarchy, + private val credentials: Credentials, + private val pruneEventTask: PruneEventTask, + private val taskExecutor: TaskExecutor) : RealmLiveEntityObserver(monarchy) { override val query = Monarchy.Query { EventEntity.where(it, type = EventType.REDACTION) } override fun processChanges(inserted: List, updated: List, deleted: List) { + Timber.v("Event pruner called with ${inserted.size} insertions") val redactionEvents = inserted - .mapNotNull { it.asDomain().redacts } + .mapNotNull { it.asDomain() } - val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents) - val workData = WorkerParamsFactory.toData(pruneEventWorkerParams) + val params = PruneEventTask.Params( + redactionEvents, + credentials.userId + ) - val sendWork = OneTimeWorkRequestBuilder() - .setInputData(workData) - .build() + pruneEventTask.configureWith(params) + .executeBy(taskExecutor) - WorkManager.getInstance() - .beginUniqueWork(PRUNE_EVENT_WORKER, ExistingWorkPolicy.APPEND, sendWork) - .enqueue() } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt new file mode 100644 index 00000000..ef8557ae --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt @@ -0,0 +1,120 @@ +/* + * 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.prune + +import arrow.core.Try +import com.zhuinden.monarchy.Monarchy +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.UnsignedData +import im.vector.matrix.android.api.session.room.send.SendState +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.EventEntity +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.tryTransactionSync +import io.realm.Realm +import timber.log.Timber + + +internal interface PruneEventTask : Task { + + data class Params( + val redactionEvents: List, + val userId: String + ) + +} + +internal class DefaultPruneEventTask( + private val monarchy: Monarchy) : PruneEventTask { + + override suspend fun execute(params: PruneEventTask.Params): Try { + return monarchy.tryTransactionSync { realm -> + params.redactionEvents.forEach { event -> + pruneEvent(realm, event, params.userId) + } + } + } + + private fun pruneEvent(realm: Realm, redactionEvent: Event, userId: String) { + if (redactionEvent.redacts.isNullOrBlank()) { + return + } + + val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId + ?: "").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 + + val allowedKeys = computeAllowedKeys(eventToPrune.type) + if (allowedKeys.isNotEmpty()) { + val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } + eventToPrune.content = ContentMapper.map(prunedContent) + } else { + when (eventToPrune.type) { + EventType.MESSAGE -> { + Timber.d("REDACTION for message ${eventToPrune.eventId}") + val unsignedData = EventMapper.map(eventToPrune).unsignedData + ?: UnsignedData(null, null) + + //was this event a m.replace +// val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() +// if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { +// eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) +// } + + val modified = unsignedData.copy(redactedEvent = redactionEvent) + eventToPrune.content = ContentMapper.map(emptyMap()) + eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) + + } +// EventType.REACTION -> { +// eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId) +// } + } + } + } + + + private fun computeAllowedKeys(type: String): List { + // Add filtered content, allowed keys in content depends on the event type + return when (type) { + EventType.STATE_ROOM_MEMBER -> listOf("membership") + 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") + EventType.STATE_ROOM_ALIASES -> listOf("aliases") + EventType.STATE_CANONICAL_ALIAS -> listOf("alias") + EventType.FEEDBACK -> listOf("type", "target_event_id") + else -> emptyList() + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt deleted file mode 100644 index 245aa551..00000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.matrix.android.internal.session.room.prune - -import android.content.Context -import androidx.work.Worker -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.internal.database.mapper.ContentMapper -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.di.MatrixKoinComponent -import im.vector.matrix.android.internal.util.WorkerParamsFactory -import im.vector.matrix.android.internal.util.tryTransactionSync -import io.realm.Realm -import org.koin.standalone.inject - -internal class PruneEventWorker(context: Context, - workerParameters: WorkerParameters -) : Worker(context, workerParameters), MatrixKoinComponent { - - @JsonClass(generateAdapter = true) - internal class Params( - val eventIdsToRedact: List - ) - - private val monarchy by inject() - - override fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.failure() - - val result = monarchy.tryTransactionSync { realm -> - params.eventIdsToRedact.forEach { eventId -> - pruneEvent(realm, eventId) - } - } - return result.fold({ Result.retry() }, { Result.success() }) - } - - private fun pruneEvent(realm: Realm, eventIdToRedact: String) { - if (eventIdToRedact.isEmpty()) { - return - } - - val eventToPrune = EventEntity.where(realm, eventId = eventIdToRedact).findFirst() - ?: return - - val allowedKeys = computeAllowedKeys(eventToPrune.type) - if (allowedKeys.isNotEmpty()) { - val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } - eventToPrune.content = ContentMapper.map(prunedContent) - } - } - - private fun computeAllowedKeys(type: String): List { - // Add filtered content, allowed keys in content depends on the event type - return when (type) { - EventType.STATE_ROOM_MEMBER -> listOf("membership") - 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") - EventType.STATE_ROOM_ALIASES -> listOf("aliases") - EventType.STATE_CANONICAL_ALIAS -> listOf("alias") - EventType.FEEDBACK -> listOf("type", "target_event_id") - else -> emptyList() - } - } - -} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index e1ca59e5..30bbe30c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -27,8 +27,8 @@ import im.vector.matrix.android.internal.util.fetchCopied internal class DefaultReadService(private val roomId: String, private val monarchy: Monarchy, - private val setReadMarkersTask: SetReadMarkersTask, - private val taskExecutor: TaskExecutor) : ReadService { + private val taskExecutor: TaskExecutor, + private val setReadMarkersTask: SetReadMarkersTask) : ReadService { override fun markAllAsRead(callback: MatrixCallback) { val latestEvent = getLatestEvent() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt new file mode 100644 index 00000000..6d6e4763 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -0,0 +1,188 @@ +/* + * 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 androidx.work.OneTimeWorkRequest +import com.zhuinden.monarchy.Monarchy +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.message.MessageType +import im.vector.matrix.android.api.session.room.model.relation.RelationService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.database.helper.addSendingEvent +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory +import im.vector.matrix.android.internal.session.room.send.RedactEventWorker +import im.vector.matrix.android.internal.session.room.send.SendEventWorker +import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.CancelableWork +import im.vector.matrix.android.internal.util.WorkerParamsFactory +import im.vector.matrix.android.internal.util.tryTransactionAsync +import timber.log.Timber + + +internal class DefaultRelationService(private val roomId: String, + private val eventFactory: LocalEchoEventFactory, + private val findReactionEventForUndoTask: FindReactionEventForUndoTask, + private val updateQuickReactionTask: UpdateQuickReactionTask, + private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor) + : RelationService { + + + override fun sendReaction(reaction: String, targetEventId: String): Cancelable { + val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) + .also { + saveLocalEcho(it) + } + val sendRelationWork = createSendRelationWork(event) + TimelineSendEventWorkCommon.postWork(roomId, sendRelationWork) + return CancelableWork(sendRelationWork.id) + } + + + private fun createSendRelationWork(event: Event): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params( + roomId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return TimelineSendEventWorkCommon.createWork(sendWorkData) + + } + + override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ { + + val params = FindReactionEventForUndoTask.Params( + roomId, + targetEventId, + reaction, + myUserId + ) + findReactionEventForUndoTask.configureWith(params) + .enableRetry() + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: FindReactionEventForUndoTask.Result) { + if (data.redactEventId == null) { + Timber.w("Cannot find reaction to undo (not yet synced?)") + //TODO? + } + data.redactEventId?.let { toRedact -> + + val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also { + saveLocalEcho(it) + } + val redactWork = createRedactEventWork(redactEvent, toRedact, null) + + TimelineSendEventWorkCommon.postWork(roomId, redactWork) + + } + } + }) + .executeBy(taskExecutor) + + } + + + override fun updateQuickReaction(reaction: String, oppositeReaction: String, targetEventId: String, myUserId: String) { + + val params = UpdateQuickReactionTask.Params( + roomId, + targetEventId, + reaction, + oppositeReaction, + myUserId + ) + + updateQuickReactionTask.configureWith(params) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: UpdateQuickReactionTask.Result) { + data.reactionToAdd?.also { sendReaction(it, targetEventId) } + data.reactionToRedact.forEach { + val redactEvent = eventFactory.createRedactEvent(roomId, it, null).also { + saveLocalEcho(it) + } + val redactWork = createRedactEventWork(redactEvent, it, null) + TimelineSendEventWorkCommon.postWork(roomId, redactWork) + } + } + }) + .executeBy(taskExecutor) + } + + private fun buildWorkIdentifier(identifier: String): String { + return "${roomId}_$identifier" + } + + //TODO duplicate with send service? + private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest { + + val sendContentWorkerParams = RedactEventWorker.Params(localEvent.eventId!!, + roomId, eventId, reason) + val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + return TimelineSendEventWorkCommon.createWork(redactWorkData) + } + + override fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable { + val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, MessageType.MSGTYPE_TEXT, compatibilityBodyText).also { + saveLocalEcho(it) + } + val sendContentWorkerParams = SendEventWorker.Params(roomId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + //TODO use relation API? + + val workRequest = TimelineSendEventWorkCommon.createWork(sendWorkData) + TimelineSendEventWorkCommon.postWork(roomId, workRequest) + return CancelableWork(workRequest.id) + + } + + + override fun replyToMessage(eventReplied: Event, replyText: String): Cancelable? { + val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText)?.also { + saveLocalEcho(it) + } ?: return null + val sendContentWorkerParams = SendEventWorker.Params(roomId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + + val workRequest = TimelineSendEventWorkCommon.createWork(sendWorkData) + TimelineSendEventWorkCommon.postWork(roomId, workRequest) + return CancelableWork(workRequest.id) + } + + /** + * Saves the event in database as a local echo. + * SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room. + * The sendingTimelineEvents is checked on new sync and will remove the local echo if an event with + * the same transaction id is received (in unsigned data) + */ + private fun saveLocalEcho(event: Event) { + monarchy.tryTransactionAsync { realm -> + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() + ?: return@tryTransactionAsync + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId) + ?: return@tryTransactionAsync + + roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt new file mode 100644 index 00000000..07b48bd0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FindReactionEventForUndoTask.kt @@ -0,0 +1,75 @@ +/* + * 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 com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.task.Task +import io.realm.Realm + + +internal interface FindReactionEventForUndoTask : Task { + + data class Params( + val roomId: String, + val eventId: String, + val reaction: String, + val myUserId: String + ) + + data class Result( + val redactEventId: String? + ) + +} + +internal class DefaultFindReactionEventForUndoTask(private val monarchy: Monarchy) : FindReactionEventForUndoTask { + + override suspend fun execute(params: FindReactionEventForUndoTask.Params): Try { + return Try { + var eventId: String? = null + monarchy.doWithRealm { realm -> + eventId = getReactionToRedact(realm, params.reaction, params.eventId, params.myUserId)?.eventId + } + FindReactionEventForUndoTask.Result(eventId) + } + } + + private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String, userId: String): EventEntity? { + val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + if (summary != null) { + summary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction) + .findFirst()?.let { + //want to find the event orignated by me! + it.sourceEvents.forEach { + //find source event + EventEntity.where(realm, it).findFirst()?.let { eventEntity -> + //is it mine? + if (eventEntity.sender == userId) { + return eventEntity + } + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt new file mode 100644 index 00000000..41184aaf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/SendRelationWorker.kt @@ -0,0 +1,81 @@ +/* + * 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 android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.failure.Failure +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.relation.ReactionContent +import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo +import im.vector.matrix.android.internal.di.MatrixKoinComponent +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.session.room.send.SendResponse +import im.vector.matrix.android.internal.util.WorkerParamsFactory +import org.koin.standalone.inject + +class SendRelationWorker(context: Context, params: WorkerParameters) + : Worker(context, params), MatrixKoinComponent { + + + @JsonClass(generateAdapter = true) + internal data class Params( + val roomId: String, + val event: Event, + val relationType: String? = null + ) + + private val roomAPI by inject() + + override fun doWork(): Result { + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.failure() + + val localEvent = params.event + if (localEvent.eventId == null) { + return Result.failure() + } + val relationContent = localEvent.content.toModel() + ?: return Result.failure() + val relatedEventId = relationContent.relatesTo?.eventId ?: return Result.failure() + val relationType = (relationContent.relatesTo as? ReactionInfo)?.type ?: params.relationType + ?: return Result.failure() + + val result = executeRequest { + apiCall = roomAPI.sendRelation( + roomId = params.roomId, + parent_id = relatedEventId, + relationType = relationType, + eventType = localEvent.type, + content = localEvent.content + ) + } + return result.fold({ + when (it) { + is Failure.NetworkConnection -> Result.retry() + else -> { + //TODO mark as failed to send? + //always return success, or the chain will be stuck for ever! + Result.success() + } + } + }, { Result.success() }) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt new file mode 100644 index 00000000..bc54464f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/UpdateQuickReactionTask.kt @@ -0,0 +1,89 @@ +/* + * 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 com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.task.Task +import io.realm.Realm + + +internal interface UpdateQuickReactionTask : Task { + + data class Params( + val roomId: String, + val eventId: String, + val reaction: String, + val oppositeReaction: String, + val myUserId: String + ) + + data class Result( + val reactionToAdd: String?, + val reactionToRedact: List + ) +} + +internal class DefaultUpdateQuickReactionTask(private val monarchy: Monarchy) : UpdateQuickReactionTask { + override suspend fun execute(params: UpdateQuickReactionTask.Params): Try { + return Try { + var res: Pair?>? = null + monarchy.doWithRealm { realm -> + res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId, params.myUserId) + } + UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList()) + } + } + + + private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String, myUserId: String): Pair?> { + //the emoji reaction has been selected, we need to check if we have reacted it or not + val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + ?: return Pair(reaction, null) + + //Ok there is already reactions on this event, have we reacted to it + val aggregationForReaction = existingSummary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction) + .findFirst() + val aggregationForOppositeReaction = existingSummary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, oppositeReaction) + .findFirst() + + if (aggregationForReaction == null || !aggregationForReaction.addedByMe) { + //i haven't yet reacted to it, so need to add it, but do I need to redact the opposite? + val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull { + //find source event + val entity = EventEntity.where(realm, it).findFirst() + if (entity?.sender == myUserId) entity.eventId else null + } + return Pair(reaction, toRedact) + } else { + //I already added it, so i need to undo it (like a toggle) + // find all m.redaction coming from me to readact them + val toRedact = aggregationForReaction.sourceEvents.mapNotNull { + //find source event + val entity = EventEntity.where(realm, it).findFirst() + if (entity?.sender == myUserId) entity.eventId else null + } + return Pair(null, toRedact) + } + + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index e8a68017..62239a74 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -16,13 +16,7 @@ package im.vector.matrix.android.internal.session.room.send -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequest -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager +import androidx.work.* import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.crypto.CryptoService @@ -31,19 +25,13 @@ import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.addTo -import im.vector.matrix.android.internal.database.helper.addSendingEvent -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom -import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.content.UploadContentWorker +import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.util.WorkerParamsFactory -import im.vector.matrix.android.internal.util.tryTransactionAsync import timber.log.Timber import java.util.concurrent.TimeUnit -private const val SEND_WORK = "SEND_WORK" private const val UPLOAD_WORK = "UPLOAD_WORK" private const val BACKOFF_DELAY = 10_000L @@ -52,46 +40,39 @@ private val WORK_CONSTRAINTS = Constraints.Builder() .build() internal class DefaultSendService(private val roomId: String, - private val eventFactory: LocalEchoEventFactory, + private val localEchoEventFactory: LocalEchoEventFactory, private val cryptoService: CryptoService, private val monarchy: Monarchy) : SendService { - override fun sendTextMessage(text: String, msgType: String): Cancelable { - val event = eventFactory.createTextEvent(roomId, msgType, text).also { + override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable { + val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also { saveLocalEcho(it) } - // Encrypted room handling - if (cryptoService.isRoomEncrypted(roomId)) { + return if (cryptoService.isRoomEncrypted(roomId)) { Timber.v("Send event in encrypted room") - // Encrypt then send - val encryptWork = createEncryptEventWork(event) - - val sendWork = OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() - - WorkManager.getInstance() - // Encrypt - .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, encryptWork) - // then send - .then(sendWork) - .enqueue() - - return CancelableWork(encryptWork.id) + val sendWork = createSendEventWork(event) + TimelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork) + CancelableWork(encryptWork.id) } else { - return sendEvent(event) + sendEvent(event) } } private fun sendEvent(event: Event): Cancelable { val sendWork = createSendEventWork(event) - WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork) - .enqueue() + TimelineSendEventWorkCommon.postWork(roomId, sendWork) + return CancelableWork(sendWork.id) + } + + override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable { + val event = localEchoEventFactory.createFormattedTextEvent(roomId, text, formattedText).also { + saveLocalEcho(it) + } + val sendWork = createSendEventWork(event) + TimelineSendEventWorkCommon.postWork(roomId, sendWork) return CancelableWork(sendWork.id) } @@ -103,9 +84,16 @@ internal class DefaultSendService(private val roomId: String, return cancelableBag } + override fun redactEvent(event: Event, reason: String?): Cancelable { + //TODO manage media/attachements? + val redactWork = createRedactEventWork(event, reason) + TimelineSendEventWorkCommon.postWork(roomId, redactWork) + return CancelableWork(redactWork.id) + } + override fun sendMedia(attachment: ContentAttachmentData): Cancelable { // Create an event with the media file path - val event = eventFactory.createMediaEvent(roomId, attachment).also { + val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { saveLocalEcho(it) } val uploadWork = createUploadMediaWork(event, attachment) @@ -120,14 +108,7 @@ internal class DefaultSendService(private val roomId: String, } private fun saveLocalEcho(event: Event) { - monarchy.tryTransactionAsync { realm -> - val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - ?: return@tryTransactionAsync - val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId) - ?: return@tryTransactionAsync - - roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) - } + localEchoEventFactory.saveLocalEcho(monarchy, event) } private fun buildWorkIdentifier(identifier: String): String { @@ -150,11 +131,16 @@ internal class DefaultSendService(private val roomId: String, val sendContentWorkerParams = SendEventWorker.Params(roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - return OneTimeWorkRequestBuilder() - .setConstraints(WORK_CONSTRAINTS) - .setInputData(sendWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + return TimelineSendEventWorkCommon.createWork(sendWorkData) + } + + private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { + val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { + saveLocalEcho(it) + } + val sendContentWorkerParams = RedactEventWorker.Params(redactEvent.eventId!!, roomId, event.eventId, reason) + val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + return TimelineSendEventWorkCommon.createWork(redactWorkData) } private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 778a49ec..e54af1ca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -17,31 +17,98 @@ package im.vector.matrix.android.internal.session.room.send import android.media.MediaMetadataRetriever +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.R import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.content.ContentAttachmentData -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.toContent -import im.vector.matrix.android.api.session.room.model.message.AudioInfo -import im.vector.matrix.android.api.session.room.model.message.FileInfo -import im.vector.matrix.android.api.session.room.model.message.ImageInfo -import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent -import im.vector.matrix.android.api.session.room.model.message.MessageFileContent -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageTextContent -import im.vector.matrix.android.api.session.room.model.message.MessageType -import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent -import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo -import im.vector.matrix.android.api.session.room.model.message.VideoInfo +import im.vector.matrix.android.api.session.events.model.* +import im.vector.matrix.android.api.session.room.model.message.* +import im.vector.matrix.android.api.session.room.model.relation.ReactionContent +import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent +import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent +import im.vector.matrix.android.internal.database.helper.addSendingEvent +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.content.ThumbnailExtractor +import im.vector.matrix.android.internal.util.StringProvider +import im.vector.matrix.android.internal.util.tryTransactionAsync +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer +import java.util.* -internal class LocalEchoEventFactory(private val credentials: Credentials) { +/** + * Creates local echo of events for room events. + * A local echo is an event that is persisted even if not yet sent to the server, + * in an optimistic way (as if the server as responded immediately). Local echo are using a local id, + * (the transaction ID), this id is used when receiving an event from a sync to check if this event + * is matching an existing local echo. + * + * The transactionID is used as loc + */ +internal class LocalEchoEventFactory(private val credentials: Credentials, private val stringProvider: StringProvider) { - fun createTextEvent(roomId: String, msgType: String, text: String): Event { + fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event { + if (autoMarkdown && msgType == MessageType.MSGTYPE_TEXT) { + val parser = Parser.builder().build() + val document = parser.parse(text) + val renderer = HtmlRenderer.builder().build() + val htmlText = renderer.render(document) + if (isFormattedTextPertinent(text, htmlText)) { //FIXME + return createFormattedTextEvent(roomId, text, htmlText) + } + } val content = MessageTextContent(type = msgType, body = text) return createEvent(roomId, content) } + private fun isFormattedTextPertinent(text: String, htmlText: String?) = + text != htmlText && htmlText != "

$text

\n" + + fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event { + val content = MessageTextContent( + type = MessageType.MSGTYPE_TEXT, + format = MessageType.FORMAT_MATRIX_HTML, + body = text, + formattedBody = formattedText + ) + return createEvent(roomId, content) + } + + + fun createReplaceTextEvent(roomId: String, targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, msgType: String, compatibilityText: String): Event { + + var newContent = MessageTextContent( + type = MessageType.MSGTYPE_TEXT, + body = newBodyText + ) + if (newBodyAutoMarkdown) { + val parser = Parser.builder().build() + val document = parser.parse(newBodyText) + val renderer = HtmlRenderer.builder().build() + val htmlText = renderer.render(document) + if (isFormattedTextPertinent(newBodyText, htmlText)) { + newContent = MessageTextContent( + type = MessageType.MSGTYPE_TEXT, + format = MessageType.FORMAT_MATRIX_HTML, + body = newBodyText, + formattedBody = htmlText + ) + } + } + + val content = MessageTextContent( + type = msgType, + body = compatibilityText, + relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), + newContent = newContent.toContent() + ) + return createEvent(roomId, content) + } + fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event { return when (attachment.type) { ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment) @@ -51,6 +118,27 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) { } } + fun createReactionEvent(roomId: String, targetEventId: String, reaction: String): Event { + val content = ReactionContent( + ReactionInfo( + RelationType.ANNOTATION, + targetEventId, + reaction + ) + ) + val localId = dummyEventId(roomId) + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + sender = credentials.userId, + eventId = localId, + type = EventType.REACTION, + content = content.toContent(), + unsignedData = UnsignedData(age = null, transactionId = localId)) + + } + + private fun createImageEvent(roomId: String, attachment: ContentAttachmentData): Event { val content = MessageImageContent( type = MessageType.MSGTYPE_IMAGE, @@ -129,13 +217,15 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) { } private fun createEvent(roomId: String, content: Any? = null): Event { + val localID = dummyEventId(roomId) return Event( roomId = roomId, originServerTs = dummyOriginServerTs(), sender = credentials.userId, - eventId = dummyEventId(roomId), + eventId = localID, type = EventType.MESSAGE, - content = content.toContent() + content = content.toContent(), + unsignedData = UnsignedData(age = null, transactionId = localID) ) } @@ -144,6 +234,120 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) { } private fun dummyEventId(roomId: String): String { - return roomId + "-" + dummyOriginServerTs() + return "m.${UUID.randomUUID()}" } + + fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? { + //Fallbacks and event representation + //TODO Add error/warning logs when any of this is null + val permalink = PermalinkFactory.createPermalink(eventReplied) ?: return null + val userId = eventReplied.sender ?: return null + val userLink = PermalinkFactory.createPermalink(userId) ?: return null +// +//
+// In reply to +// @alice:example.org +//
+// +//
+//
+// This is where the reply goes. + val body = bodyForReply(eventReplied.content.toModel()) + val replyFallbackTemplateFormatted = """ +
${stringProvider.getString(R.string.message_reply_to_prefix)}%s
%s
%s + """.trimIndent().format(permalink, userLink, userId, body.second ?: body.first, replyText) +// +// > <@alice:example.org> This is the original body +// +// This is where the reply goes + val lines = body.first.split("\n") + val plainTextBody = StringBuffer("><${userId}>") + lines.firstOrNull()?.also { plainTextBody.append(" $it") } + lines.forEachIndexed { index, s -> + if (index > 0) { + plainTextBody.append("\n>$s") + } + } + plainTextBody.append("\n\n").append(replyText) + + val eventId = eventReplied.eventId ?: return null + val content = MessageTextContent( + type = MessageType.MSGTYPE_TEXT, + format = MessageType.FORMAT_MATRIX_HTML, + body = plainTextBody.toString(), + formattedBody = replyFallbackTemplateFormatted, + relatesTo = RelationDefaultContent(null, null, ReplyToContent(eventId)) + ) + return createEvent(roomId, content) + } + + /** + * Returns a pair of used for the fallback event representation + * in a reply message. + */ + private fun bodyForReply(content: MessageContent?): Pair { + when (content?.type) { + MessageType.MSGTYPE_EMOTE, + MessageType.MSGTYPE_TEXT, + MessageType.MSGTYPE_NOTICE -> { + //If we already have formatted body, return it? + var formattedText: String? = null + if (content is MessageTextContent) { + if (content.format == MessageType.FORMAT_MATRIX_HTML) { + formattedText = content.formattedBody + } + } + return content.body to formattedText + } + MessageType.MSGTYPE_FILE -> return stringProvider.getString(R.string.reply_to_a_file) to null + MessageType.MSGTYPE_AUDIO -> return stringProvider.getString(R.string.reply_to_an_audio_file) to null + MessageType.MSGTYPE_IMAGE -> return stringProvider.getString(R.string.reply_to_an_image) to null + MessageType.MSGTYPE_VIDEO -> return stringProvider.getString(R.string.reply_to_a_video) to null + else -> return (content?.body ?: "") to null + + } + + } + + /* + * { + "content": { + "reason": "Spamming" + }, + "event_id": "$143273582443PhrSn:domain.com", + "origin_server_ts": 1432735824653, + "redacts": "$fukweghifu23:localhost", + "room_id": "!jEsUZKDJdhlrceRyVU:domain.com", + "sender": "@example:domain.com", + "type": "m.room.redaction", + "unsigned": { + "age": 1234 + } + } + */ + fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event { + val localID = dummyEventId(roomId) + return Event( + roomId = roomId, + originServerTs = dummyOriginServerTs(), + sender = credentials.userId, + eventId = localID, + type = EventType.REDACTION, + redacts = eventId, + content = reason?.let { mapOf("reason" to it).toContent() }, + unsignedData = UnsignedData(age = null, transactionId = localID) + ) + } + + fun saveLocalEcho(monarchy: Monarchy, event: Event) { + monarchy.tryTransactionAsync { realm -> + val roomEntity = RoomEntity.where(realm, roomId = event.roomId!!).findFirst() + ?: return@tryTransactionAsync + val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = event.roomId) + ?: return@tryTransactionAsync + + roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) + } + } + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt new file mode 100644 index 00000000..5056a93b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.session.room.send + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.internal.di.MatrixKoinComponent +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.util.WorkerParamsFactory +import org.koin.standalone.inject + +internal class RedactEventWorker(context: Context, params: WorkerParameters) + : Worker(context, params), MatrixKoinComponent { + + @JsonClass(generateAdapter = true) + internal data class Params( + val txID: String, + val roomId: String, + val eventId: String, + val reason: String? + ) + + private val roomAPI by inject() + + override fun doWork(): Result { + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.failure() + + val eventId = params.eventId + val result = executeRequest { + apiCall = roomAPI.redactEvent( + params.txID, + params.roomId, + eventId, + if (params.reason == null) emptyMap() else mapOf("reason" to params.reason) + ) + } + return result.fold({ + when (it) { + is Failure.NetworkConnection -> Result.retry() + else -> { + //TODO mark as failed to send? + //always return success, or the chain will be stuck for ever! + Result.success() + } + } + }, { + Result.success() + }) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt index f595b8db..078dc588 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt @@ -20,20 +20,12 @@ import android.content.Context import androidx.work.Worker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass -import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.send.SendState -import im.vector.matrix.android.internal.database.helper.addSendingEvent -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.RoomEntity -import im.vector.matrix.android.internal.database.query.find -import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom -import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.util.WorkerParamsFactory -import im.vector.matrix.android.internal.util.tryTransactionAsync import org.koin.standalone.inject internal class SendEventWorker(context: Context, params: WorkerParameters) @@ -47,12 +39,11 @@ internal class SendEventWorker(context: Context, params: WorkerParameters) ) private val roomAPI by inject() - private val monarchy by inject() override fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success() + ?: return Result.success() val event = params.event if (event.eventId == null) { @@ -67,15 +58,15 @@ internal class SendEventWorker(context: Context, params: WorkerParameters) event.content ) } - return result.fold( - { - //TODO export that in a localEchoRepository - monarchy.tryTransactionAsync { realm -> - val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() - val eventEntity = roomEntity?.sendingTimelineEvents?.find(event.eventId) - eventEntity?.sendState = SendState.UNSENT - } - Result.success() } - , { Result.success() }) + return result.fold({ + when (it) { + is Failure.NetworkConnection -> Result.retry() + else -> { + //TODO mark as failed to send? + //always return success, or the chain will be stuck for ever! + Result.success() + } + } + }, { Result.success() }) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt index b471bf19..f1ad712e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/state/DefaultStateService.kt @@ -23,8 +23,8 @@ import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith internal class DefaultStateService(private val roomId: String, - private val sendStateTask: SendStateTask, - private val taskExecutor: TaskExecutor) : StateService { + private val taskExecutor: TaskExecutor, + private val sendStateTask: SendStateTask) : StateService { override fun updateTopic(topic: String, callback: MatrixCallback) { val params = SendStateTask.Params(roomId, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 58274288..6743d1cb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -27,29 +27,22 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.api.util.addTo -import im.vector.matrix.android.internal.database.model.ChunkEntity -import im.vector.matrix.android.internal.database.model.ChunkEntityFields -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.model.EventEntityFields -import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.mapper.asDomain +import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.query.findIncludingEvent import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.database.query.whereInRoom import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer -import io.realm.OrderedCollectionChangeSet -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.RealmQuery -import io.realm.RealmResults -import io.realm.Sort +import io.realm.* import timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import kotlin.collections.ArrayList +import kotlin.collections.HashMap private const val INITIAL_LOAD_SIZE = 20 @@ -92,19 +85,23 @@ internal class DefaultTimeline( private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN private val isLive = initialEventId == null private val builtEvents = Collections.synchronizedList(ArrayList()) + private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) private val backwardsPaginationState = AtomicReference(PaginationState()) private val forwardsPaginationState = AtomicReference(PaginationState()) - private val eventsChangeListener = OrderedRealmCollectionChangeListener> { _, changeSet -> - if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL ) { + private lateinit var eventRelations: RealmResults + + private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> + if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { handleInitialLoad() } else { // If changeSet has deletion we are having a gap, so we clear everything - if(changeSet.deletionRanges.isNotEmpty()){ + if (changeSet.deletionRanges.isNotEmpty()) { prevDisplayIndex = DISPLAY_INDEX_UNKNOWN nextDisplayIndex = DISPLAY_INDEX_UNKNOWN builtEvents.clear() + builtEventsIdMap.clear() timelineEventFactory.clear() } changeSet.insertionRanges.forEach { range -> @@ -125,9 +122,43 @@ internal class DefaultTimeline( buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) postSnapshot() } + } + var hasChanged = false + changeSet.changes.forEach {index -> + val eventEntity = results[index] + eventEntity?.eventId?.let { eventId -> + builtEventsIdMap[eventId]?.let { builtIndex -> + //Update the relation of existing event + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = timelineEventFactory.create(eventEntity) + hasChanged = true + } + } + } + } + if (hasChanged) postSnapshot() + } + } + + private val relationsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> + + var hasChange = false + + (changeSet.insertions + changeSet.changes).forEach { + val eventRelations = collection[it] + if (eventRelations != null) { + builtEventsIdMap[eventRelations.eventId]?.let { builtIndex -> + //Update the relation of existing event + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = te.copy(annotations = eventRelations.asDomain()) + hasChange = true + } + } } } + if (hasChange) + postSnapshot() } // Public methods ****************************************************************************** @@ -146,6 +177,7 @@ internal class DefaultTimeline( } } + override fun start() { if (isStarted.compareAndSet(false, true)) { Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") @@ -171,6 +203,10 @@ internal class DefaultTimeline( .also { it.addChangeListener(eventsChangeListener) } isReady.set(true) + + eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId) + .findAllAsync() + .also { it.addChangeListener(relationsListener) } } } } @@ -242,6 +278,7 @@ internal class DefaultTimeline( } else { updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } } + return !shouldFetchMore } @@ -266,14 +303,14 @@ internal class DefaultTimeline( private fun getPaginationState(direction: Timeline.Direction): PaginationState { return when (direction) { - Timeline.Direction.FORWARDS -> forwardsPaginationState.get() + Timeline.Direction.FORWARDS -> forwardsPaginationState.get() Timeline.Direction.BACKWARDS -> backwardsPaginationState.get() } } private fun updatePaginationState(direction: Timeline.Direction, update: (PaginationState) -> PaginationState) { val stateReference = when (direction) { - Timeline.Direction.FORWARDS -> forwardsPaginationState + Timeline.Direction.FORWARDS -> forwardsPaginationState Timeline.Direction.BACKWARDS -> backwardsPaginationState } val currentValue = stateReference.get() @@ -316,9 +353,9 @@ internal class DefaultTimeline( private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) ?: return val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") paginationTask.configureWith(params) @@ -384,6 +421,9 @@ internal class DefaultTimeline( val timelineEvent = timelineEventFactory.create(eventEntity) val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size builtEvents.add(position, timelineEvent) + //Need to shift :/ + builtEventsIdMap.entries.filter { it.value >= position }.forEach { it.setValue(it.value + 1) } + builtEventsIdMap[eventEntity.eventId] = position } Timber.v("Built ${offsetResults.size} items from db") return offsetResults.size diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt index 8b949721..c8629ecc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -28,8 +28,8 @@ import im.vector.matrix.android.internal.util.fetchCopyMap internal class DefaultTimelineService(private val roomId: String, private val monarchy: Monarchy, private val taskExecutor: TaskExecutor, - private val contextOfEventTask: GetContextOfEventTask, private val timelineEventFactory: TimelineEventFactory, + private val contextOfEventTask: GetContextOfEventTask, private val paginationTask: PaginationTask ) : TimelineService { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt index bc0a453a..7c4cfff9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt @@ -16,22 +16,29 @@ package im.vector.matrix.android.internal.session.room.timeline -import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.internal.crypto.MXDecryptionException import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor +import im.vector.matrix.android.internal.session.room.EventRelationExtractor +import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor import io.realm.Realm import timber.log.Timber import java.util.* -internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomMemberExtractor, - private val cryptoService: CryptoService) { +/** + * This class is responsible for building [TimelineEvent] returned by a [Timeline] through [TimelineService] + * It handles decryption, extracting additional data around an event as sender data and relation. + */ +internal class TimelineEventFactory( + private val roomMemberExtractor: SenderRoomMemberExtractor, + private val relationExtractor: EventRelationExtractor, + private val cryptoService: CryptoService) { private val timelineId = UUID.randomUUID().toString() private val senderCache = mutableMapOf() @@ -39,7 +46,7 @@ internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomM fun create(eventEntity: EventEntity, realm: Realm = eventEntity.realm): TimelineEvent { val sender = eventEntity.sender - val cacheKey = sender + eventEntity.stateIndex + val cacheKey = sender + eventEntity.localId val senderData = senderCache.getOrPut(cacheKey) { val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity, realm) SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl) @@ -48,19 +55,22 @@ internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomM if (event.getClearType() == EventType.ENCRYPTED) { handleEncryptedEvent(event, eventEntity.localId) } + + val relations = relationExtractor.extractFrom(eventEntity, realm) return TimelineEvent( event, eventEntity.localId, eventEntity.displayIndex, senderData.senderName, senderData.senderAvatar, - eventEntity.sendState + eventEntity.sendState, + relations ) } - private fun handleEncryptedEvent(event: Event, eventId: String) { + private fun handleEncryptedEvent(event: Event, cacheKey: String) { Timber.v("Encrypted event: try to decrypt ${event.eventId}") - val cachedDecryption = decryptionCache[eventId] + val cachedDecryption = decryptionCache[cacheKey] if (cachedDecryption != null) { Timber.v("Encrypted event ${event.eventId} cached") event.setClearData(cachedDecryption) @@ -68,7 +78,7 @@ internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomM try { val result = cryptoService.decryptEvent(event, timelineId) if (result != null) { - decryptionCache[eventId] = result + decryptionCache[cacheKey] = result } event.setClearData(result) } catch (failure: Throwable) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt new file mode 100644 index 00000000..2bfafdbd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -0,0 +1,73 @@ +/* + * 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.timeline + +import androidx.work.* +import java.util.concurrent.TimeUnit + + +private const val SEND_WORK = "SEND_WORK" +private const val BACKOFF_DELAY = 10_000L + +private val WORK_CONSTRAINTS = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + +/** + * Helper class for sending event related works. + * All send event from a room are using the same workchain, in order to ensure order. + * WorkRequest must always return success (even if server error, in this case marking the event as failed to send) + * , if not the chain will be doomed in failed state. + * + */ +internal object TimelineSendEventWorkCommon { + + fun postSequentialWorks(roomId: String, vararg workRequests: OneTimeWorkRequest) { + when { + workRequests.isEmpty() -> return + workRequests.size == 1 -> postWork(roomId, workRequests.first()) + else -> { + val firstWork = workRequests.first() + var continuation = WorkManager.getInstance() + .beginUniqueWork(buildWorkIdentifier(roomId), ExistingWorkPolicy.APPEND, firstWork) + for (i in 1 until workRequests.size) { + val workRequest = workRequests[i] + continuation = continuation.then(workRequest) + } + continuation.enqueue() + } + } + } + + fun postWork(roomId: String, workRequest: OneTimeWorkRequest) { + WorkManager.getInstance() + .beginUniqueWork(buildWorkIdentifier(roomId), ExistingWorkPolicy.APPEND, workRequest) + .enqueue() + + } + + inline fun createWork(data: Data): OneTimeWorkRequest { + return OneTimeWorkRequestBuilder() + .setConstraints(WORK_CONSTRAINTS) + .setInputData(data) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun buildWorkIdentifier(roomId: String): String { + return "${roomId}_$SEND_WORK" + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index 283b807c..9c40b4aa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -18,12 +18,7 @@ package im.vector.matrix.android.internal.session.room.timeline import arrow.core.Try import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.internal.database.helper.addAll -import im.vector.matrix.android.internal.database.helper.addOrUpdate -import im.vector.matrix.android.internal.database.helper.addStateEvents -import im.vector.matrix.android.internal.database.helper.deleteOnCascade -import im.vector.matrix.android.internal.database.helper.isUnlinked -import im.vector.matrix.android.internal.database.helper.merge +import im.vector.matrix.android.internal.database.helper.* import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.create @@ -116,7 +111,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction") val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) val nextToken: String? val prevToken: String? @@ -139,7 +134,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { } else { nextChunk?.apply { this.prevToken = prevToken } } - ?: ChunkEntity.create(realm, prevToken, nextToken) + ?: ChunkEntity.create(realm, prevToken, nextToken) if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { Timber.v("Reach end of $roomId") @@ -147,6 +142,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { } else { Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}") currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked()) + // Then we merge chunks if needed if (currentChunk != prevChunk && prevChunk != null) { currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt index eb985759..25bea096 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/GroupSyncHandler.kt @@ -17,7 +17,7 @@ package im.vector.matrix.android.internal.session.sync import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.room.model.MyMembership +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.sync.model.GroupsSyncResponse @@ -56,7 +56,7 @@ internal class GroupSyncHandler(private val monarchy: Monarchy) { groupId: String): GroupEntity { val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) - groupEntity.membership = MyMembership.JOINED + groupEntity.membership = Membership.JOIN return groupEntity } @@ -64,7 +64,7 @@ internal class GroupSyncHandler(private val monarchy: Monarchy) { groupId: String): GroupEntity { val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) - groupEntity.membership = MyMembership.INVITED + groupEntity.membership = Membership.INVITE return groupEntity } @@ -74,7 +74,7 @@ internal class GroupSyncHandler(private val monarchy: Monarchy) { groupId: String): GroupEntity { val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId) - groupEntity.membership = MyMembership.LEFT + groupEntity.membership = Membership.LEAVE return groupEntity } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 102eabc1..4de16067 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -20,7 +20,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel -import im.vector.matrix.android.api.session.room.model.MyMembership +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent import im.vector.matrix.android.internal.crypto.CryptoManager import im.vector.matrix.android.internal.database.helper.addAll @@ -63,9 +63,9 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy) { val rooms = when (handlingStrategy) { - is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) } + is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) } is HandlingStrategy.INVITED -> handlingStrategy.data.map { handleInvitedRoom(realm, it.key, it.value) } - is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(it.key, it.value) } + is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(realm, it.key, it.value) } } realm.insertOrUpdate(rooms) } @@ -79,11 +79,10 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) - if (roomEntity.membership == MyMembership.INVITED) { + if (roomEntity.membership == Membership.INVITE) { roomEntity.chunks.deleteAllFromRealm() } - - roomEntity.membership = MyMembership.JOINED + roomEntity.membership = Membership.JOIN val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) val isInitialSync = lastChunk == null @@ -124,11 +123,14 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, transactionIds.forEach { val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it) if (sendingEventEntity != null) { + Timber.v("Remove local echo for tx:$it") roomEntity.sendingTimelineEvents.remove(sendingEventEntity) + } else { + Timber.v("Can't find corresponding local echo for tx:$it") } } } - roomSummaryUpdater.update(realm, roomId, roomSync.summary, roomSync.unreadNotifications) + roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications) if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) { handleEphemeral(realm, roomId, roomSync.ephemeral) @@ -137,7 +139,6 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) { handleRoomAccountDataEvents(realm, roomId, roomSync.accountData) } - return roomEntity } @@ -145,26 +146,28 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, roomId: String, roomSync: InvitedRoomSync): RoomEntity { - Timber.v("Handle invited sync for room $roomId") - - val roomEntity = RoomEntity() - roomEntity.roomId = roomId - roomEntity.membership = MyMembership.INVITED + val roomEntity = RoomEntity.where(realm, roomId).findFirst() + ?: realm.createObject(roomId) + roomEntity.membership = Membership.INVITE if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) { val chunkEntity = handleTimelineEvents(realm, roomId, roomSync.inviteState.events) roomEntity.addOrUpdate(chunkEntity) } + roomSummaryUpdater.update(realm, roomId, Membership.INVITE) return roomEntity } - // TODO : handle it - private fun handleLeftRoom(roomId: String, + private fun handleLeftRoom(realm: Realm, + roomId: String, roomSync: RoomSync): RoomEntity { - return RoomEntity().apply { - this.roomId = roomId - this.membership = MyMembership.LEFT - } + val roomEntity = RoomEntity.where(realm, roomId).findFirst() + ?: realm.createObject(roomId) + + roomEntity.membership = Membership.LEAVE + roomEntity.chunks.deleteAllFromRealm() + roomSummaryUpdater.update(realm, roomId, Membership.LEAVE, roomSync.summary, roomSync.unreadNotifications) + return roomEntity } private fun handleTimelineEvents(realm: Realm, @@ -183,6 +186,9 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, lastChunk?.isLastForward = false chunkEntity.isLastForward = true chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset) + + //update eventAnnotationSummary here? + return chunkEntity } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt index b8e1f028..e3f68f06 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/DefaultUserService.kt @@ -18,9 +18,13 @@ package im.vector.matrix.android.internal.session.user +import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.model.User +import im.vector.matrix.android.internal.database.RealmLiveData +import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.util.fetchCopied @@ -29,12 +33,19 @@ internal class DefaultUserService(private val monarchy: Monarchy) : UserService override fun getUser(userId: String): User? { val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } - ?: return null + ?: return null - return User( - userEntity.userId, - userEntity.displayName, - userEntity.avatarUrl - ) + return userEntity.asDomain() + } + + override fun observeUser(userId: String): LiveData { + val liveRealmData = RealmLiveData(monarchy.realmConfiguration) { realm -> + UserEntity.where(realm, userId) + } + return Transformations.map(liveRealmData) { results -> + results + .map { it.asDomain() } + .firstOrNull() + } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UpdateUserTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UpdateUserTask.kt index e2e423cd..3230ecf0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UpdateUserTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UpdateUserTask.kt @@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.UserEntity import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.session.room.members.RoomMembers +import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.tryTransactionSync diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt new file mode 100644 index 00000000..02d90c2f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringProvider.kt @@ -0,0 +1,55 @@ +/* + * 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.util + +import android.content.res.Resources +import androidx.annotation.NonNull +import androidx.annotation.StringRes + +internal class StringProvider(private val resources: Resources) { + + /** + * Returns a localized string from the application's package's + * default string table. + * + * @param resId Resource id for the string + * @return The string data associated with the resource, stripped of styled + * text information. + */ + @NonNull + fun getString(@StringRes resId: Int): String { + return resources.getString(resId) + } + + /** + * Returns a localized formatted string from the application's package's + * default string table, substituting the format arguments as defined in + * [java.util.Formatter] and [java.lang.String.format]. + * + * @param resId Resource id for the format string + * @param formatArgs The format arguments that will be used for + * substitution. + * @return The string data associated with the resource, formatted and + * stripped of styled text information. + */ + @NonNull + fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String { + return resources.getString(resId, *formatArgs) + } + + +} \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index 5e18183a..7bcfdf72 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -71,10 +71,21 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + signingConfigs { + debug { + keyAlias 'androiddebugkey' + keyPassword 'android' + storeFile file('./signature/debug.keystore') + storePassword 'android' + } + } + buildTypes { debug { resValue "bool", "debug_mode", "true" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" + + signingConfig signingConfigs.debug } release { @@ -121,7 +132,7 @@ dependencies { def epoxy_version = "3.3.0" def arrow_version = "0.8.2" def coroutines_version = "1.0.1" - def markwon_version = '3.0.0-SNAPSHOT' + def markwon_version = '3.0.0' def big_image_viewer_version = '1.5.6' def glide_version = '4.9.0' def moshi_version = '1.8.0' @@ -156,7 +167,7 @@ dependencies { implementation("com.airbnb.android:epoxy:$epoxy_version") kapt "com.airbnb.android:epoxy-processor:$epoxy_version" - implementation 'com.airbnb.android:mvrx:0.7.0' + implementation 'com.airbnb.android:mvrx:1.0.1' // Work implementation "android.arch.work:work-runtime-ktx:1.0.0" @@ -173,6 +184,7 @@ dependencies { implementation 'me.gujun.android:span:1.7' implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:html:$markwon_version" + implementation 'me.saket:better-link-movement-method:2.2.0' // Passphrase strength helper implementation 'com.nulab-inc:zxcvbn:1.2.5' diff --git a/vector/sampledata/matrix.json b/vector/sampledata/matrix.json new file mode 100644 index 00000000..b9c55b91 --- /dev/null +++ b/vector/sampledata/matrix.json @@ -0,0 +1,52 @@ +{ + "data": [ + { + "displayName": "Long display name useful to test layout with a long display name", + "mxid": "@longmatrixidbecausesometimesuserschooselongmxid:matrix.org", + "message": "William Shakespeare (bapt. 26 April 1564 โ€“ 23 April 1616) was an English poet, playwright and actor, widely regarded as the greatest writer in the English language and the world's greatest dramatist. He is often called England's national poet and the \"Bard of Avon\". His extant works, including collaborations, consist of approximately 39 plays, 154 sonnets, two long narrative poems, and a few other verses, some of uncertain authorship. His plays have been translated into every major living language and are performed more often than those of any other playwright.\n\nShakespeare was born and raised in Stratford-upon-Avon, Warwickshire. At the age of 18, he married Anne Hathaway, with whom he had three children: Susanna and twins Hamnet and Judith. Sometime between 1585 and 1592, he began a successful career in London as an actor, writer, and part-owner of a playing company called the Lord Chamberlain's Men, later known as the King's Men. At age 49 (around 1613), he appears to have retired to Stratford, where he died three years later. Few records of Shakespeare's private life survive; this has stimulated considerable speculation about such matters as his physical appearance, his sexuality, his religious beliefs, and whether the works attributed to him were written by others. Such theories are often criticised for failing to adequately note that few records survive of most commoners of the period.\n\nShakespeare produced most of his known works between 1589 and 1613. His early plays were primarily comedies and histories and are regarded as some of the best work produced in these genres. Until about 1608, he wrote mainly tragedies, among them Hamlet, Othello, King Lear, and Macbeth, all considered to be among the finest works in the English language. In the last phase of his life, he wrote tragicomedies (also known as romances) and collaborated with other playwrights.\n\nMany of Shakespeare's plays were published in editions of varying quality and accuracy in his lifetime. However, in 1623, two fellow actors and friends of Shakespeare's, John Heminges and Henry Condell, published a more definitive text known as the First Folio, a posthumous collected edition of Shakespeare's dramatic works that included all but two of his plays. The volume was prefaced with a poem by Ben Jonson, in which Jonson presciently hails Shakespeare in a now-famous quote as \"not of an age, but for all time\".\n\nThroughout the 20th and 21st centuries, Shakespeare's works have been continually adapted and rediscovered by new movements in scholarship and performance. His plays remain popular and are studied, performed, and reinterpreted through various cultural and political contexts around the world.", + "roomName": "Matrix HQ", + "roomAlias": "#matrix:matrix.org", + "roomTopic": "Welcome to Matrix HQ! Here is the rest of the room topicโ€ฆ" + }, + { + "displayName": "benoit", + "mxid": "@benoit:matrix.org", + "message": "Hello!", + "roomName": "Room name very loooooooong with some details", + "roomAlias": "#matrix:matrix.org", + "roomTopic": "Room topic very loooooooong with some details" + }, + { + "displayName": "ganfra", + "mxid": "@ganfra:matrix.org", + "message": "How are you?", + "roomName": "Room name very loooooooong with some details", + "roomAlias": "#matrix:matrix.org", + "roomTopic": "Room topic very loooooooong with some details" + }, + { + "displayName": "Manu", + "mxid": "@manu:matrix.org", + "message": "Great weather today!", + "roomName": "Room name very loooooooong with some details", + "roomAlias": "#matrix:matrix.org", + "roomTopic": "Room topic very loooooooong with some details" + }, + { + "displayName": "Giom", + "mxid": "@giom:matrix.org", + "message": "Let's do a picnic", + "roomName": "Room name very loooooooong with some details", + "roomAlias": "#matrix:matrix.org", + "roomTopic": "Room topic very loooooooong with some details" + }, + { + "displayName": "Nad", + "mxid": "@nadonomy:matrix.org", + "message": "Yes, great idea", + "roomName": "Room name very loooooooong with some details", + "roomAlias": "#matrix:matrix.org", + "roomTopic": "Room topic very loooooooong with some details" + } + ] +} diff --git a/vector/signature/README.md b/vector/signature/README.md new file mode 100644 index 00000000..7d9005f1 --- /dev/null +++ b/vector/signature/README.md @@ -0,0 +1,12 @@ + +## Debug signature + +Buildkite CI tool uses docker images to build the Android application, and it looks like the debug signature is changed at each build. + +So it's not possible for user to upgrade the application with the last build from buildkite without uninstalling the application. + +This folder contains a debug signature, and the debug build will uses this signature to build the APK. + +The validity of the signature is 30 years. So it has to be replaced before June 2049 :). + +More info about the debug signature: https://developer.android.com/studio/publish/app-signing#debug-mode diff --git a/vector/signature/debug.keystore b/vector/signature/debug.keystore new file mode 100644 index 00000000..4a15fc9e Binary files /dev/null and b/vector/signature/debug.keystore differ diff --git a/vector/src/debug/AndroidManifest.xml b/vector/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..debb2987 --- /dev/null +++ b/vector/src/debug/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/vector/src/debug/java/im/vector/riotredesign/features/debug/DebugMaterialThemeActivity.kt b/vector/src/debug/java/im/vector/riotredesign/features/debug/DebugMaterialThemeActivity.kt new file mode 100644 index 00000000..475427fe --- /dev/null +++ b/vector/src/debug/java/im/vector/riotredesign/features/debug/DebugMaterialThemeActivity.kt @@ -0,0 +1,65 @@ +/* + * 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.riotredesign.features.debug + +import android.os.Bundle +import android.view.Menu +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.snackbar.Snackbar +import im.vector.riotredesign.R +import im.vector.riotredesign.core.utils.toast +import kotlinx.android.synthetic.debug.activity_test_material_theme.* + +// Rendering is not the same with VectorBaseActivity +abstract class DebugMaterialThemeActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_test_material_theme) + + debugShowSnackbar.setOnClickListener { + Snackbar.make(debugMaterialCoordinator, "Snackbar!", Snackbar.LENGTH_SHORT) + .setAction("Action") { } + .show() + } + + debugShowToast.setOnClickListener { + toast("Toast") + } + + debugShowDialog.setOnClickListener { + AlertDialog.Builder(this) + .setMessage("Dialog content") + .setIcon(R.drawable.ic_settings_x) + .setPositiveButton("Positive", null) + .setNegativeButton("Negative", null) + .setNeutralButton("Neutral", null) + .show() + } + + debugShowBottomSheet.setOnClickListener { + BottomSheetDialogFragment().show(supportFragmentManager, "TAG") + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.vector_home, menu) + return true + } +} diff --git a/vector/src/debug/java/im/vector/riotredesign/features/debug/DebugMaterialThemeDarkActivity.kt b/vector/src/debug/java/im/vector/riotredesign/features/debug/DebugMaterialThemeDarkActivity.kt new file mode 100644 index 00000000..bab21417 --- /dev/null +++ b/vector/src/debug/java/im/vector/riotredesign/features/debug/DebugMaterialThemeDarkActivity.kt @@ -0,0 +1,19 @@ +/* + * 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.riotredesign.features.debug + +class DebugMaterialThemeDarkActivity : DebugMaterialThemeActivity() \ No newline at end of file diff --git a/vector/src/debug/java/im/vector/riotredesign/features/debug/DebugMaterialThemeLightActivity.kt b/vector/src/debug/java/im/vector/riotredesign/features/debug/DebugMaterialThemeLightActivity.kt new file mode 100644 index 00000000..039d7c07 --- /dev/null +++ b/vector/src/debug/java/im/vector/riotredesign/features/debug/DebugMaterialThemeLightActivity.kt @@ -0,0 +1,19 @@ +/* + * 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.riotredesign.features.debug + +class DebugMaterialThemeLightActivity : DebugMaterialThemeActivity() \ No newline at end of file diff --git a/vector/src/debug/java/im/vector/riotredesign/features/debug/DebugMenuActivity.kt b/vector/src/debug/java/im/vector/riotredesign/features/debug/DebugMenuActivity.kt new file mode 100644 index 00000000..add4e2da --- /dev/null +++ b/vector/src/debug/java/im/vector/riotredesign/features/debug/DebugMenuActivity.kt @@ -0,0 +1,137 @@ +/* + * 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.riotredesign.features.debug + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.Person +import butterknife.OnClick +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.VectorBaseActivity + + +class DebugMenuActivity : VectorBaseActivity() { + + override fun getLayoutRes() = R.layout.activity_debug_menu + + @OnClick(R.id.debug_test_text_view_link) + fun testTextViewLink() { + startActivity(Intent(this, TestLinkifyActivity::class.java)) + } + + @OnClick(R.id.debug_test_notification) + fun testNotification() { + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // Create channel first + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = + NotificationChannel( + "CHAN", + "Channel name", + NotificationManager.IMPORTANCE_DEFAULT + ) + + channel.description = "Channel description" + (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel) + + val channel2 = + NotificationChannel( + "CHAN2", + "Channel name 2", + NotificationManager.IMPORTANCE_DEFAULT + ) + + channel2.description = "Channel description 2" + (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel2) + } + + + val builder = NotificationCompat.Builder(this, "CHAN") + .setWhen(System.currentTimeMillis()) + .setContentTitle("Title") + .setContentText("Content") + // No effect because it's a group summary notif + .setNumber(33) + .setSmallIcon(R.drawable.logo_transparent) + // This provocate the badge issue: no badge for group notification + .setGroup("GroupKey") + .setGroupSummary(true) + + val messagingStyle1 = NotificationCompat.MessagingStyle( + Person.Builder() + .setName("User name") + .build() + ) + .addMessage("Message 1 - 1", System.currentTimeMillis(), Person.Builder().setName("user 1-1").build()) + .addMessage("Message 1 - 2", System.currentTimeMillis(), Person.Builder().setName("user 1-2").build()) + + val messagingStyle2 = NotificationCompat.MessagingStyle( + Person.Builder() + .setName("User name 2") + .build() + ) + .addMessage("Message 2 - 1", System.currentTimeMillis(), Person.Builder().setName("user 1-1").build()) + .addMessage("Message 2 - 2", System.currentTimeMillis(), Person.Builder().setName("user 1-2").build()) + + + notificationManager.notify(10, builder.build()) + + notificationManager.notify( + 11, + NotificationCompat.Builder(this, "CHAN") + .setChannelId("CHAN") + .setWhen(System.currentTimeMillis()) + .setContentTitle("Title 1") + .setContentText("Content 1") + // For shortcut on long press on launcher icon + .setBadgeIconType(NotificationCompat.BADGE_ICON_NONE) + .setStyle(messagingStyle1) + .setSmallIcon(R.drawable.logo_transparent) + .setGroup("GroupKey") + .build() + ) + + notificationManager.notify( + 12, + NotificationCompat.Builder(this, "CHAN2") + .setWhen(System.currentTimeMillis()) + .setContentTitle("Title 2") + .setContentText("Content 2") + .setStyle(messagingStyle2) + .setSmallIcon(R.drawable.logo_transparent) + .setGroup("GroupKey") + .build() + ) + } + + @OnClick(R.id.debug_test_material_theme_light) + fun testMaterialThemeLight() { + startActivity(Intent(this, DebugMaterialThemeLightActivity::class.java)) + } + + @OnClick(R.id.debug_test_material_theme_dark) + fun testMaterialThemeDark() { + startActivity(Intent(this, DebugMaterialThemeDarkActivity::class.java)) + } + +} + diff --git a/vector/src/debug/java/im/vector/riotredesign/features/debug/TestLinkifyActivity.kt b/vector/src/debug/java/im/vector/riotredesign/features/debug/TestLinkifyActivity.kt new file mode 100644 index 00000000..7e37c319 --- /dev/null +++ b/vector/src/debug/java/im/vector/riotredesign/features/debug/TestLinkifyActivity.kt @@ -0,0 +1,133 @@ +/* + * 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.riotredesign.features.debug + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.coordinatorlayout.widget.CoordinatorLayout +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.riotredesign.R + + +class TestLinkifyActivity : AppCompatActivity() { + + @BindView(R.id.test_linkify_content_view) + lateinit var scrollContent: LinearLayout + + @BindView(R.id.test_linkify_coordinator) + lateinit var coordinatorLayout: CoordinatorLayout + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_test_linkify) + ButterKnife.bind(this) + + scrollContent.removeAllViews() + + listOf( + "https://www.html5rocks.com/en/tutorials/webrtc/basics/ |", + "https://www.html5rocks.com/en/tutorials/webrtc/basics/", + "mailto mailto:test@toto.com test@toto.com", + "Here is the link.www.test.com/foo/?23=35 you got it?", + "www.lemonde.fr", + " /www.lemonde.fr", + "://www.lemonde.fr", + "file:///dev/null ", + " ansible/xoxys.matrix#2c0b65eb", + "foo.ansible/xoxys.matrix#2c0b65eb", + "foo.ansible.fpo/xoxys.matrix#2c0b65eb", + "https://foo.ansible.fpo/xoxys.matrix#2c0b65eb", + "@vf:matrix.org", + "+44 207 123 1234", + "+33141437940", + "1234", + "3456.34,089", + "ksks9808", + "For example: geo:48.85828,2.29449?z=16 should be clickable", + "geo:37.786971,-122.399677;u=35", + "37.786971,-122.399677;u=35", + "48.107864,-1.712153", + "synchrone peut tenir la route la", + "that.is.some.sexy.link", + "test overlap 48.107864,0673728392 geo + pn?", + "test overlap 0673728392,48.107864 geo + pn?", + "If I add a link in brackets like (help for Riot: https://about.riot.im/help), the link is usable on Riot for Desktop", + "(help for Riot: https://about.riot.im/help)", + "http://example.com/test(1).html", + "http://example.com/test(1)", + "https://about.riot.im/help)", + "(http://example.com/test(1))", + "http://example.com/test1)", + "http://example.com/test1/, et ca", + "www.example.com/, et ca", + "foo.ansible.toplevel/xoxys.matrix#2c0b65eb", + "foo.ansible.ninja/xoxys.matrix#2c0b65eb", + "in brackets like (help for Riot: https://www.exemple/com/find(1)) , the link is usable ", + """ + In brackets like (help for Riot: https://about.riot.im/help) , the link is usable, + But you can call +44 207 123 1234 and come to 37.786971,-122.399677;u=35 then + see if this mail jhon@riot.im is active but this should not 12345 + """.trimIndent() + ) + .forEach { textContent -> + val item = LayoutInflater.from(this) + .inflate(R.layout.item_test_linkify, scrollContent, false) + + item.findViewById(R.id.test_linkify_auto_text) + ?.apply { + text = textContent + /* TODO Use BetterLinkMovementMethod when the other PR is merged + movementMethod = MatrixLinkMovementMethod(object : MockMessageAdapterActionListener() { + override fun onURLClick(uri: Uri?) { + Snackbar.make(coordinatorLayout, "URI Clicked: $uri", Snackbar.LENGTH_LONG) + .setAction("open") { + openUrlInExternalBrowser(this@TestLinkifyActivity, uri) + } + .show() + } + }) + */ + } + + item.findViewById(R.id.test_linkify_custom_text) + ?.apply { + text = textContent + /* TODO Use BetterLinkMovementMethod when the other PR is merged + movementMethod = MatrixLinkMovementMethod(object : MockMessageAdapterActionListener() { + override fun onURLClick(uri: Uri?) { + Snackbar.make(coordinatorLayout, "URI Clicked: $uri", Snackbar.LENGTH_LONG) + .setAction("open") { + openUrlInExternalBrowser(this@TestLinkifyActivity, uri) + } + .show() + } + }) + */ + + // TODO Call VectorLinkify.addLinks(text) + } + + scrollContent.addView(item, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)) + } + } +} diff --git a/vector/src/main/res/drawable/bg_unread_notification.xml b/vector/src/debug/res/drawable/linear_divider.xml similarity index 59% rename from vector/src/main/res/drawable/bg_unread_notification.xml rename to vector/src/debug/res/drawable/linear_divider.xml index 496134a7..95436029 100644 --- a/vector/src/main/res/drawable/bg_unread_notification.xml +++ b/vector/src/debug/res/drawable/linear_divider.xml @@ -1,6 +1,8 @@ + + + - - \ No newline at end of file diff --git a/vector/src/debug/res/layout/activity_debug_menu.xml b/vector/src/debug/res/layout/activity_debug_menu.xml new file mode 100644 index 00000000..d1e85019 --- /dev/null +++ b/vector/src/debug/res/layout/activity_debug_menu.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/debug/res/layout/activity_test_linkify.xml b/vector/src/debug/res/layout/activity_test_linkify.xml new file mode 100644 index 00000000..2ccb2e5f --- /dev/null +++ b/vector/src/debug/res/layout/activity_test_linkify.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/debug/res/layout/activity_test_material_theme.xml b/vector/src/debug/res/layout/activity_test_material_theme.xml new file mode 100644 index 00000000..198beefb --- /dev/null +++ b/vector/src/debug/res/layout/activity_test_material_theme.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/debug/res/layout/demo_store_listing.xml b/vector/src/debug/res/layout/demo_store_listing.xml new file mode 100644 index 00000000..209a0589 --- /dev/null +++ b/vector/src/debug/res/layout/demo_store_listing.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/debug/res/layout/demo_theme_sample.xml b/vector/src/debug/res/layout/demo_theme_sample.xml new file mode 100644 index 00000000..b9102f1b --- /dev/null +++ b/vector/src/debug/res/layout/demo_theme_sample.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/debug/res/layout/demo_themes.xml b/vector/src/debug/res/layout/demo_themes.xml new file mode 100644 index 00000000..ac6bb409 --- /dev/null +++ b/vector/src/debug/res/layout/demo_themes.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/debug/res/layout/item_test_linkify.xml b/vector/src/debug/res/layout/item_test_linkify.xml new file mode 100644 index 00000000..19bebeaf --- /dev/null +++ b/vector/src/debug/res/layout/item_test_linkify.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/debug/res/values/styles.xml b/vector/src/debug/res/values/styles.xml new file mode 100644 index 00000000..063f652d --- /dev/null +++ b/vector/src/debug/res/values/styles.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index a65ef719..ac3f0725 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -55,6 +55,11 @@ android:name="im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity" android:label="@string/title_activity_emoji_reaction_picker" /> + + + + + diff --git a/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt b/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt index 4df075a0..80a9ea81 100644 --- a/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotredesign/VectorApplication.kt @@ -18,6 +18,7 @@ package im.vector.riotredesign import android.app.Application import android.content.Context +import android.content.res.Configuration import androidx.multidex.MultiDex import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyController @@ -27,11 +28,13 @@ import com.github.piasy.biv.loader.glide.GlideImageLoader import com.jakewharton.threetenabp.AndroidThreeTen import im.vector.matrix.android.api.Matrix import im.vector.riotredesign.core.di.AppModule +import im.vector.riotredesign.features.configuration.VectorConfiguration import im.vector.riotredesign.features.home.HomeModule import im.vector.riotredesign.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotredesign.features.rageshake.VectorFileLogger import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler -import org.koin.android.logger.AndroidLogger +import im.vector.riotredesign.features.roomdirectory.RoomDirectoryModule +import org.koin.android.ext.android.inject import org.koin.log.EmptyLogger import org.koin.standalone.StandAloneContext.startKoin import timber.log.Timber @@ -40,10 +43,10 @@ import timber.log.Timber class VectorApplication : Application() { lateinit var appContext: Context + val vectorConfiguration: VectorConfiguration by inject() override fun onCreate() { super.onCreate() - appContext = this VectorUncaughtExceptionHandler.activate(this) @@ -62,13 +65,11 @@ class VectorApplication : Application() { EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() val appModule = AppModule(applicationContext).definition val homeModule = HomeModule().definition - startKoin( - list = listOf(appModule, homeModule), - logger = if (BuildConfig.DEBUG) AndroidLogger() else EmptyLogger()) - + val roomDirectoryModule = RoomDirectoryModule().definition + startKoin(listOf(appModule, homeModule, roomDirectoryModule), logger = EmptyLogger()) Matrix.getInstance().setApplicationFlavor(BuildConfig.FLAVOR_DESCRIPTION) - registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks()) + vectorConfiguration.initConfiguration() } override fun attachBaseContext(base: Context) { @@ -76,4 +77,9 @@ class VectorApplication : Application() { MultiDex.install(this) } + override fun onConfigurationChanged(newConfig: Configuration?) { + super.onConfigurationChanged(newConfig) + vectorConfiguration.onConfigurationChanged(newConfig) + } + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/animations/Constants.kt b/vector/src/main/java/im/vector/riotredesign/core/animations/Constants.kt new file mode 100644 index 00000000..4d9c59aa --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/animations/Constants.kt @@ -0,0 +1,19 @@ +/* + * 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.riotredesign.core.animations + +const val ANIMATION_DURATION_SHORT = 200L diff --git a/vector/src/main/java/im/vector/riotredesign/core/animations/SimpleAnimatorListener.kt b/vector/src/main/java/im/vector/riotredesign/core/animations/SimpleAnimatorListener.kt new file mode 100644 index 00000000..f1aad8f1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/animations/SimpleAnimatorListener.kt @@ -0,0 +1,37 @@ +/* + * 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.riotredesign.core.animations + +import android.animation.Animator + +open class SimpleAnimatorListener : Animator.AnimatorListener { + override fun onAnimationRepeat(animation: Animator?) { + // No op + } + + override fun onAnimationEnd(animation: Animator?) { + // No op + } + + override fun onAnimationCancel(animation: Animator?) { + // No op + } + + override fun onAnimationStart(animation: Animator?) { + // No op + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/animations/SimpleTransitionListener.kt b/vector/src/main/java/im/vector/riotredesign/core/animations/SimpleTransitionListener.kt new file mode 100644 index 00000000..0e644990 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/animations/SimpleTransitionListener.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.animations + +import androidx.transition.Transition + +open class SimpleTransitionListener : Transition.TransitionListener { + override fun onTransitionEnd(transition: Transition) { + // No op + } + + override fun onTransitionResume(transition: Transition) { + // No op + } + + override fun onTransitionPause(transition: Transition) { + // No op + } + + override fun onTransitionCancel(transition: Transition) { + // No op + } + + override fun onTransitionStart(transition: Transition) { + // No op + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/animations/VectorFullTransitionSet.kt b/vector/src/main/java/im/vector/riotredesign/core/animations/VectorFullTransitionSet.kt new file mode 100644 index 00000000..666e6ced --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/animations/VectorFullTransitionSet.kt @@ -0,0 +1,44 @@ +/* + * 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.riotredesign.core.animations + +import android.content.Context +import android.util.AttributeSet +import androidx.transition.ChangeBounds +import androidx.transition.ChangeTransform +import androidx.transition.Fade +import androidx.transition.TransitionSet + +class VectorFullTransitionSet : TransitionSet { + + constructor() { + init() + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init() + } + + private fun init() { + ordering = ORDERING_TOGETHER + addTransition(Fade(Fade.OUT)) + .addTransition(ChangeBounds()) + .addTransition(ChangeTransform()) + .addTransition(Fade(Fade.IN)) + } + +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt index 308b900f..68db4f68 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/di/AppModule.kt @@ -18,15 +18,20 @@ package im.vector.riotredesign.core.di import android.content.Context import android.content.Context.MODE_PRIVATE +import androidx.fragment.app.Fragment import im.vector.matrix.android.api.Matrix -import im.vector.riotredesign.core.resources.ColorProvider +import im.vector.riotredesign.core.error.ErrorFormatter import im.vector.riotredesign.core.resources.LocaleProvider +import im.vector.riotredesign.core.resources.StringArrayProvider import im.vector.riotredesign.core.resources.StringProvider +import im.vector.riotredesign.features.configuration.VectorConfiguration import im.vector.riotredesign.features.crypto.verification.IncomingVerificationRequestHandler +import im.vector.riotredesign.features.home.HomeRoomListObservableStore import im.vector.riotredesign.features.home.group.SelectedGroupStore -import im.vector.riotredesign.features.home.room.VisibleRoomStore -import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository -import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator +import im.vector.riotredesign.features.home.room.list.AlphabeticalRoomComparator +import im.vector.riotredesign.features.home.room.list.ChronologicalRoomComparator +import im.vector.riotredesign.features.navigation.DefaultNavigator +import im.vector.riotredesign.features.navigation.Navigator import im.vector.riotredesign.features.notifications.NotificationDrawerManager import org.koin.dsl.module.module @@ -34,6 +39,10 @@ class AppModule(private val context: Context) { val definition = module { + single { + VectorConfiguration(context) + } + single { LocaleProvider(context.resources) } @@ -43,27 +52,31 @@ class AppModule(private val context: Context) { } single { - ColorProvider(context) + StringArrayProvider(context.resources) } single { context.getSharedPreferences("im.vector.riot", MODE_PRIVATE) } - single { - RoomSelectionRepository(get()) - } - single { SelectedGroupStore() } single { - VisibleRoomStore() + HomeRoomListObservableStore() } single { - RoomSummaryComparator() + ChronologicalRoomComparator() + } + + single { + AlphabeticalRoomComparator() + } + + single { + ErrorFormatter(get()) } single { @@ -78,6 +91,8 @@ class AppModule(private val context: Context) { IncomingVerificationRequestHandler(context, get()) } - + factory { (fragment: Fragment) -> + DefaultNavigator(fragment) as Navigator + } } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/epoxy/ErrorWithRetryItem.kt b/vector/src/main/java/im/vector/riotredesign/core/epoxy/ErrorWithRetryItem.kt new file mode 100644 index 00000000..c6bfb13a --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/epoxy/ErrorWithRetryItem.kt @@ -0,0 +1,46 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.riotredesign.core.epoxy + +import android.widget.Button +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotredesign.R + +@EpoxyModelClass(layout = R.layout.item_error_retry) +abstract class ErrorWithRetryItem : VectorEpoxyModel() { + + @EpoxyAttribute + var text: String? = null + + @EpoxyAttribute + var listener: (() -> Unit)? = null + + override fun bind(holder: Holder) { + holder.textView.text = text + holder.buttonView.setOnClickListener { listener?.invoke() } + } + + + class Holder : VectorEpoxyHolder() { + val textView by bind(R.id.itemErrorRetryText) + val buttonView by bind