diff --git a/CHANGES.md b/CHANGES.md
index e93d1bb089..cf885d5cd5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,14 @@
+Changes in Element v1.3.15 (2022-01-18)
+=======================================
+
+Bugfixes 🐛
+----------
+ - Fix crash when viewing source which contains an emoji ([#4796](https://github.com/vector-im/element-android/issues/4796))
+ - Prevent crash in Timeline and add more logs. ([#4959](https://github.com/vector-im/element-android/issues/4959))
+ - Fix crash on API <24 and make sure this error will not occur again. ([#4962](https://github.com/vector-im/element-android/issues/4962))
+ - Fixes sign in/up crash when selecting ems and other server types which use SSO ([#4969](https://github.com/vector-im/element-android/issues/4969))
+
+
Changes in Element v1.3.14 (2022-01-12)
=======================================
diff --git a/dependencies.gradle b/dependencies.gradle
index b4e1a7e7fa..77d072e7c7 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -71,6 +71,7 @@ ext.libs = [
'espressoIntents' : "androidx.test.espresso:espresso-intents:$espresso"
],
google : [
+ // TODO There is 1.6.0?
'material' : "com.google.android.material:material:1.4.0"
],
dagger : [
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index 3853919bcb..fd36f5110c 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -4,7 +4,6 @@ ext.groups = [
],
group: [
'com.github.Armen101',
- 'com.github.BillCarsonFr',
'com.github.chrisbanes',
'com.github.hyuwah',
'com.github.jetradarmobile',
@@ -154,6 +153,7 @@ ext.groups = [
'org.jetbrains.intellij.deps',
'org.jetbrains.kotlin',
'org.jetbrains.kotlinx',
+ 'org.json',
'org.jsoup',
'org.junit',
'org.junit.jupiter',
diff --git a/fastlane/metadata/android/en-US/changelogs/40103150.txt b/fastlane/metadata/android/en-US/changelogs/40103150.txt
new file mode 100644
index 0000000000..2b5fbe76ca
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/40103150.txt
@@ -0,0 +1,2 @@
+Main changes in this version: First change in onboarding screens, including Analytics opt-in. Support for Events with Math added in the labs.
+Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.15
\ No newline at end of file
diff --git a/library/jsonviewer/.gitignore b/library/jsonviewer/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/library/jsonviewer/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/library/jsonviewer/build.gradle b/library/jsonviewer/build.gradle
new file mode 100644
index 0000000000..ee2be6fd25
--- /dev/null
+++ b/library/jsonviewer/build.gradle
@@ -0,0 +1,64 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-parcelize'
+apply plugin: 'kotlin-kapt'
+apply plugin: 'com.jakewharton.butterknife'
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
+ }
+}
+
+android {
+ compileSdk versions.compileSdk
+
+ defaultConfig {
+ minSdk versions.minSdk
+ targetSdk versions.targetSdk
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility versions.sourceCompat
+ targetCompatibility versions.targetCompat
+ }
+
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+ implementation libs.androidx.appCompat
+ implementation libs.androidx.core
+
+ implementation libs.airbnb.epoxy
+ kapt libs.airbnb.epoxyProcessor
+
+ implementation libs.airbnb.mavericks
+ // Span utils
+ implementation 'me.gujun.android:span:1.7'
+
+ implementation libs.google.material
+
+ implementation libs.jetbrains.coroutinesCore
+ implementation libs.jetbrains.coroutinesAndroid
+
+ testImplementation 'org.json:json:20190722'
+ testImplementation libs.tests.junit
+ androidTestImplementation libs.androidx.junit
+ androidTestImplementation libs.androidx.espressoCore
+}
diff --git a/library/jsonviewer/src/main/AndroidManifest.xml b/library/jsonviewer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..73322c2fdb
--- /dev/null
+++ b/library/jsonviewer/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt
new file mode 100644
index 0000000000..a8d9cac849
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerDialog.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2022 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 org.billcarsonfr.jsonviewer
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import androidx.fragment.app.DialogFragment
+import com.airbnb.mvrx.Mavericks
+
+class JSonViewerDialog : DialogFragment() {
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_dialog_jv, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val args: JSonViewerFragmentArgs = arguments?.getParcelable(Mavericks.KEY_ARG) ?: return
+ if (savedInstanceState == null) {
+ childFragmentManager.beginTransaction()
+ .replace(
+ R.id.fragmentContainer, JSonViewerFragment.newInstance(
+ args.jsonString,
+ args.defaultOpenDepth,
+ true,
+ args.styleProvider
+ )
+ )
+ .commitNow()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ // Get existing layout params for the window
+ val params = dialog?.window?.attributes
+ // Assign window properties to fill the parent
+ params?.width = WindowManager.LayoutParams.MATCH_PARENT
+ params?.height = WindowManager.LayoutParams.MATCH_PARENT
+ dialog?.window?.attributes = params
+ }
+
+ companion object {
+ fun newInstance(
+ jsonString: String,
+ initialOpenDepth: Int = -1,
+ styleProvider: JSonViewerStyleProvider? = null
+ ): JSonViewerDialog {
+ val args = Bundle()
+ val parcelableArgs =
+ JSonViewerFragmentArgs(jsonString, initialOpenDepth, false, styleProvider)
+ args.putParcelable(Mavericks.KEY_ARG, parcelableArgs)
+ return JSonViewerDialog().apply { arguments = args }
+ }
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt
new file mode 100644
index 0000000000..9c48a137da
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerEpoxyController.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright (c) 2022 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 org.billcarsonfr.jsonviewer
+
+import android.content.Context
+import android.view.View
+import com.airbnb.epoxy.TypedEpoxyController
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Success
+import me.gujun.android.span.Span
+import me.gujun.android.span.span
+
+internal class JSonViewerEpoxyController(private val context: Context) :
+ TypedEpoxyController() {
+
+ private var styleProvider: JSonViewerStyleProvider = JSonViewerStyleProvider.default(context)
+
+ fun setStyle(styleProvider: JSonViewerStyleProvider?) {
+ this.styleProvider = styleProvider ?: JSonViewerStyleProvider.default(context)
+ }
+
+ override fun buildModels(data: JSonViewerState?) {
+ val async = data?.root ?: return
+
+ when (async) {
+ is Fail -> {
+ valueItem {
+ id("fail")
+ text(async.error.localizedMessage?.toSafeCharSequence())
+ }
+ }
+ is Success -> {
+ val model = data.root.invoke()
+
+ model?.let {
+ buildRec(it, 0, "")
+ }
+ }
+ }
+ }
+
+ private fun buildRec(
+ model: JSonViewerModel,
+ depth: Int,
+ idBase: String
+ ) {
+ val host = this
+ val id = "$idBase/${model.key ?: model.index}_${model.isExpanded}}"
+ when (model) {
+ is JSonViewerObject -> {
+ if (model.isExpanded) {
+ open(id, model.key, model.index, depth, true, model)
+ model.keys.forEach {
+ buildRec(it.value, depth + 1, id)
+ }
+ close(id, depth, true)
+ } else {
+ valueItem {
+ id(id + "_sum")
+ depth(depth)
+ text(
+ span {
+ if (model.key != null) {
+ span("\"${model.key}\"") {
+ textColor = host.styleProvider.keyColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
+ }
+ if (model.index != null) {
+ span("${model.index}") {
+ textColor = host.styleProvider.secondaryColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
+ }
+ span {
+ +"{+${model.keys.size}}"
+ textColor = host.styleProvider.baseColor
+ }
+ }.toSafeCharSequence()
+ )
+ itemClickListener(View.OnClickListener { host.itemClicked(model) })
+ }
+ }
+ }
+ is JSonViewerArray -> {
+ if (model.isExpanded) {
+ open(id, model.key, model.index, depth, false, model)
+ model.items.forEach {
+ buildRec(it, depth + 1, id)
+ }
+ close(id, depth, false)
+ } else {
+ valueItem {
+ id(id + "_sum")
+ depth(depth)
+ text(
+ span {
+ if (model.key != null) {
+ span("\"${model.key}\"") {
+ textColor = host.styleProvider.keyColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
+ }
+ if (model.index != null) {
+ span("${model.index}") {
+ textColor = host.styleProvider.secondaryColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
+ }
+ span {
+ +"[+${model.items.size}]"
+ textColor = host.styleProvider.baseColor
+ }
+ }.toSafeCharSequence()
+ )
+ itemClickListener(View.OnClickListener { host.itemClicked(model) })
+ }
+ }
+ }
+ is JSonViewerLeaf -> {
+ valueItem {
+ id(id)
+ depth(depth)
+ text(
+ span {
+ if (model.key != null) {
+ span("\"${model.key}\"") {
+ textColor = host.styleProvider.keyColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
+ }
+
+ if (model.index != null) {
+ span("${model.index}") {
+ textColor = host.styleProvider.secondaryColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
+ }
+ append(host.valueToSpan(model))
+ }.toSafeCharSequence()
+ )
+ copyValue(model.stringRes)
+ }
+ }
+ }
+ }
+
+ private fun valueToSpan(leaf: JSonViewerLeaf): Span {
+ val host = this
+ return when (leaf.type) {
+ JSONType.STRING -> {
+ span("\"${leaf.stringRes}\"") {
+ textColor = host.styleProvider.stringColor
+ }
+ }
+ JSONType.NUMBER -> {
+ span(leaf.stringRes) {
+ textColor = host.styleProvider.numberColor
+ }
+ }
+ JSONType.BOOLEAN -> {
+ span(leaf.stringRes) {
+ textColor = host.styleProvider.booleanColor
+ }
+ }
+ JSONType.NULL -> {
+ span("null") {
+ textColor = host.styleProvider.booleanColor
+ }
+ }
+ }
+ }
+
+ private fun open(
+ id: String,
+ key: String?,
+ index: Int?,
+ depth: Int,
+ isObject: Boolean = true,
+ composed: JSonViewerModel
+ ) {
+ val host = this
+ valueItem {
+ id("${id}_Open")
+ depth(depth)
+ text(
+ span {
+ if (key != null) {
+ span("\"$key\"") {
+ textColor = host.styleProvider.keyColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
+ }
+ if (index != null) {
+ span("$index") {
+ textColor = host.styleProvider.secondaryColor
+ }
+ span(" : ") {
+ textColor = host.styleProvider.baseColor
+ }
+ }
+ span("- ") {
+ textColor = host.styleProvider.secondaryColor
+ }
+ span("{".takeIf { isObject } ?: "[") {
+ textColor = host.styleProvider.baseColor
+ }
+ }.toSafeCharSequence()
+ )
+ itemClickListener(View.OnClickListener { host.itemClicked(composed) })
+ }
+ }
+
+ private fun itemClicked(model: JSonViewerModel) {
+ model.isExpanded = !model.isExpanded
+ setData(currentData)
+ }
+
+ private fun close(id: String, depth: Int, isObject: Boolean = true) {
+ val host = this
+ valueItem {
+ id("${id}_Close")
+ depth(depth)
+ text(
+ span {
+ text = "}".takeIf { isObject } ?: "]"
+ textColor = host.styleProvider.baseColor
+ }.toSafeCharSequence()
+ )
+ }
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt
new file mode 100644
index 0000000000..51e2797958
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerFragment.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2022 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 org.billcarsonfr.jsonviewer
+
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.airbnb.epoxy.EpoxyRecyclerView
+import com.airbnb.mvrx.Mavericks
+import com.airbnb.mvrx.MavericksView
+import com.airbnb.mvrx.fragmentViewModel
+import com.airbnb.mvrx.withState
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+internal data class JSonViewerFragmentArgs(
+ val jsonString: String,
+ val defaultOpenDepth: Int,
+ val wrap: Boolean,
+ val styleProvider: JSonViewerStyleProvider?
+) : Parcelable
+
+class JSonViewerFragment : Fragment(), MavericksView {
+
+ private val viewModel: JSonViewerViewModel by fragmentViewModel()
+
+ private val epoxyController by lazy {
+ JSonViewerEpoxyController(requireContext())
+ }
+
+ private lateinit var recyclerView: EpoxyRecyclerView
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val args: JSonViewerFragmentArgs? = arguments?.getParcelable(Mavericks.KEY_ARG)
+ val inflate =
+ if (args?.wrap == true) {
+ inflater.inflate(R.layout.fragment_jv_recycler_view_wrap, container, false)
+ } else {
+ inflater.inflate(R.layout.fragment_jv_recycler_view, container, false)
+ }
+ recyclerView = inflate.findViewById(R.id.jvRecyclerView)
+ recyclerView.layoutManager =
+ LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
+ recyclerView.setController(epoxyController)
+ epoxyController.setStyle(args?.styleProvider)
+ registerForContextMenu(recyclerView)
+ return inflate
+ }
+
+ fun showJson(jsonString: String, initialOpenDepth: Int) {
+ viewModel.setJsonSource(jsonString, initialOpenDepth)
+ }
+
+ override fun invalidate() = withState(viewModel) { state ->
+ epoxyController.setData(state)
+ }
+
+ companion object {
+ fun newInstance(
+ jsonString: String,
+ initialOpenDepth: Int = -1,
+ wrap: Boolean = false,
+ styleProvider: JSonViewerStyleProvider? = null
+ ): JSonViewerFragment {
+ return JSonViewerFragment().apply {
+ arguments = Bundle().apply {
+ putParcelable(
+ Mavericks.KEY_ARG,
+ JSonViewerFragmentArgs(
+ jsonString,
+ initialOpenDepth,
+ wrap,
+ styleProvider
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt
new file mode 100644
index 0000000000..3d1f8dd3e2
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerModel.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2022 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 org.billcarsonfr.jsonviewer
+
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+internal open class JSonViewerModel(var key: String?, var index: Int?, val jObject: Any) {
+ var depth = 0
+ var isExpanded = false
+}
+
+internal interface Composed {
+ fun addChild(model: JSonViewerModel)
+}
+
+internal class JSonViewerObject(key: String?, index: Int?, jObject: JSONObject) :
+ JSonViewerModel(key, index, jObject),
+ Composed {
+
+ var keys = LinkedHashMap()
+
+ override fun addChild(model: JSonViewerModel) {
+ keys[model.key!!] = model
+ }
+}
+
+internal class JSonViewerArray(key: String?, index: Int?, jObject: JSONArray) :
+ JSonViewerModel(key, index, jObject), Composed {
+ var items = ArrayList()
+
+ override fun addChild(model: JSonViewerModel) {
+ items.add(model)
+ }
+}
+
+internal class JSonViewerLeaf(key: String?, index: Int?, val stringRes: String, val type: JSONType) :
+ JSonViewerModel(key, index, stringRes)
+
+internal enum class JSONType {
+ STRING,
+ NUMBER,
+ BOOLEAN,
+ NULL
+}
+
+internal object ModelParser {
+
+ @Throws(JSONException::class)
+ fun fromJsonString(jsonString: String, initialOpenDepth: Int = -1): JSonViewerObject {
+ val jobj = JSONObject(jsonString.trim())
+ val root = JSonViewerObject(null, null, jobj).apply { isExpanded = true }
+ jobj.keys().forEach {
+ eval(root, it, null, jobj.get(it), 1, initialOpenDepth)
+ }
+ return root
+ }
+
+ private fun eval(parent: Composed, key: String?, index: Int?, obj: Any, depth: Int, initialOpenDepth: Int) {
+ when (obj) {
+ is JSONObject -> {
+ val objectComposed = JSonViewerObject(key, index, obj)
+ .apply { isExpanded = initialOpenDepth == -1 || depth <= initialOpenDepth }
+ objectComposed.depth = depth
+ obj.keys().forEach {
+ eval(objectComposed, it, null, obj.get(it), depth + 1, initialOpenDepth)
+ }
+ parent.addChild(objectComposed)
+ }
+ is JSONArray -> {
+ val objectComposed = JSonViewerArray(key, index, obj)
+ .apply { isExpanded = initialOpenDepth == -1 || depth <= initialOpenDepth }
+ objectComposed.depth = depth
+ for (i in 0 until obj.length()) {
+ eval(objectComposed, null, i, obj[i], depth + 1, initialOpenDepth)
+ }
+ parent.addChild(objectComposed)
+ }
+ is String -> {
+ JSonViewerLeaf(key, index, obj, JSONType.STRING).let {
+ it.depth = depth
+ parent.addChild(it)
+ }
+ }
+ is Number -> {
+ JSonViewerLeaf(key, index, obj.toString(), JSONType.NUMBER).let {
+ it.depth = depth
+ parent.addChild(it)
+ }
+ }
+ is Boolean -> {
+ JSonViewerLeaf(key, index, obj.toString(), JSONType.BOOLEAN).let {
+ it.depth = depth
+ parent.addChild(it)
+ }
+ }
+ else -> {
+ if (obj == JSONObject.NULL) {
+ JSonViewerLeaf(key, index, "null", JSONType.NULL).let {
+ it.depth = depth
+ parent.addChild(it)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt
new file mode 100644
index 0000000000..4fc04c91e4
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerStyleProvider.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2022 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 org.billcarsonfr.jsonviewer
+
+import android.content.Context
+import android.os.Parcelable
+import androidx.annotation.ColorInt
+import androidx.core.content.ContextCompat
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class JSonViewerStyleProvider(
+ @ColorInt val keyColor: Int,
+ @ColorInt val stringColor: Int,
+ @ColorInt val booleanColor: Int,
+ @ColorInt val numberColor: Int,
+ @ColorInt val baseColor: Int,
+ @ColorInt val secondaryColor: Int
+) : Parcelable {
+
+ companion object {
+ fun default(context: Context) = JSonViewerStyleProvider(
+ keyColor = ContextCompat.getColor(context, R.color.key_color),
+ stringColor = ContextCompat.getColor(context, R.color.string_color),
+ booleanColor = ContextCompat.getColor(context, R.color.bool_color),
+ numberColor = ContextCompat.getColor(context, R.color.number_color),
+ baseColor = ContextCompat.getColor(context, R.color.base_color),
+ secondaryColor = ContextCompat.getColor(context, R.color.secondary_color)
+ )
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt
new file mode 100644
index 0000000000..bc3f022cfa
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/JSonViewerViewModel.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2022 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 org.billcarsonfr.jsonviewer
+
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.MavericksState
+import com.airbnb.mvrx.MavericksViewModel
+import com.airbnb.mvrx.MavericksViewModelFactory
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import com.airbnb.mvrx.ViewModelContext
+import kotlinx.coroutines.launch
+
+internal data class JSonViewerState(
+ val root: Async = Uninitialized
+) : MavericksState
+
+internal class JSonViewerViewModel(initialState: JSonViewerState) :
+ MavericksViewModel(initialState) {
+
+ fun setJsonSource(json: String, initialOpenDepth: Int) {
+ setState {
+ copy(root = Loading())
+ }
+ viewModelScope.launch {
+ try {
+ ModelParser.fromJsonString(json, initialOpenDepth).let {
+ setState {
+ copy(
+ root = Success(it)
+ )
+ }
+ }
+ } catch (error: Throwable) {
+ setState {
+ copy(
+ root = Fail(error)
+ )
+ }
+ }
+ }
+ }
+
+ companion object : MavericksViewModelFactory {
+
+ @JvmStatic
+ override fun initialState(viewModelContext: ViewModelContext): JSonViewerState? {
+ val arg: JSonViewerFragmentArgs = viewModelContext.args()
+ return try {
+ JSonViewerState(
+ Success(ModelParser.fromJsonString(arg.jsonString, arg.defaultOpenDepth))
+ )
+ } catch (failure: Throwable) {
+ JSonViewerState(Fail(failure))
+ }
+ }
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/SafeCharSequence.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/SafeCharSequence.kt
new file mode 100644
index 0000000000..79556f81d7
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/SafeCharSequence.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2022 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 org.billcarsonfr.jsonviewer
+
+/**
+ * Wrapper for a CharSequence, which support mutation of the CharSequence, which can happen during rendering
+ * TODO Mutualize
+ */
+internal class SafeCharSequence(val charSequence: CharSequence) {
+ private val hash = charSequence.toString().hashCode()
+
+ override fun hashCode() = hash
+ override fun equals(other: Any?) = other is SafeCharSequence && other.hash == hash
+}
+
+internal fun CharSequence.toSafeCharSequence() = SafeCharSequence(this)
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt
new file mode 100644
index 0000000000..6536a3401e
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/Utils.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2022 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 org.billcarsonfr.jsonviewer
+
+import android.content.Context
+import android.util.TypedValue
+
+/**
+ * TODO Mutualize
+ */
+internal object Utils {
+ fun dpToPx(dp: Int, context: Context): Int {
+ return TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ dp.toFloat(),
+ context.resources.displayMetrics
+ ).toInt()
+ }
+}
diff --git a/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt
new file mode 100644
index 0000000000..9193a20ab2
--- /dev/null
+++ b/library/jsonviewer/src/main/java/org/billcarsonfr/jsonviewer/ValueItem.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2022 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 org.billcarsonfr.jsonviewer
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.view.ContextMenu
+import android.view.Menu
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyHolder
+import com.airbnb.epoxy.EpoxyModelClass
+import com.airbnb.epoxy.EpoxyModelWithHolder
+
+@EpoxyModelClass(layout = R2.layout.item_jv_base_value)
+internal abstract class ValueItem : EpoxyModelWithHolder() {
+
+ @EpoxyAttribute
+ var text: SafeCharSequence? = null
+
+ @EpoxyAttribute
+ var depth: Int = 0
+
+ @EpoxyAttribute
+ var copyValue: String? = null
+
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var itemClickListener: View.OnClickListener? = null
+
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.textView.text = text?.charSequence
+ holder.baseView.setPadding(Utils.dpToPx(16 * depth, holder.baseView.context), 0, 0, 0)
+ itemClickListener?.let { holder.baseView.setOnClickListener(it) }
+ holder.copyValue = copyValue
+ }
+
+ override fun unbind(holder: Holder) {
+ super.unbind(holder)
+ holder.baseView.setOnClickListener(null)
+ holder.copyValue = null
+ }
+
+ class Holder : EpoxyHolder(), View.OnCreateContextMenuListener {
+
+ lateinit var textView: TextView
+ lateinit var baseView: LinearLayout
+ var copyValue: String? = null
+
+ override fun bindView(itemView: View) {
+ baseView = itemView.findViewById(R.id.jvBaseLayout)
+ textView = itemView.findViewById(R.id.jvValueText)
+ itemView.setOnCreateContextMenuListener(this)
+ }
+
+ override fun onCreateContextMenu(
+ menu: ContextMenu?,
+ v: View?,
+ menuInfo: ContextMenu.ContextMenuInfo?
+ ) {
+ if (copyValue != null) {
+ val menuItem = menu?.add(
+ Menu.NONE, R.id.copy_value,
+ Menu.NONE, R.string.copy_value
+ )
+ val clipService =
+ v?.context?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
+ menuItem?.setOnMenuItemClickListener {
+ clipService?.setPrimaryClip(ClipData.newPlainText("", copyValue))
+ true
+ }
+ }
+ }
+ }
+}
diff --git a/library/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml b/library/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml
new file mode 100644
index 0000000000..fb9e6d38c5
--- /dev/null
+++ b/library/jsonviewer/src/main/res/layout/fragment_dialog_jv.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml b/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml
new file mode 100644
index 0000000000..20822191e6
--- /dev/null
+++ b/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml b/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml
new file mode 100644
index 0000000000..8b61b13111
--- /dev/null
+++ b/library/jsonviewer/src/main/res/layout/fragment_jv_recycler_view_wrap.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/library/jsonviewer/src/main/res/layout/item_jv_base_value.xml b/library/jsonviewer/src/main/res/layout/item_jv_base_value.xml
new file mode 100644
index 0000000000..b7dee1221b
--- /dev/null
+++ b/library/jsonviewer/src/main/res/layout/item_jv_base_value.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/library/jsonviewer/src/main/res/menu/jv_menu_item.xml b/library/jsonviewer/src/main/res/menu/jv_menu_item.xml
new file mode 100644
index 0000000000..4da69b5117
--- /dev/null
+++ b/library/jsonviewer/src/main/res/menu/jv_menu_item.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/library/jsonviewer/src/main/res/values/colors.xml b/library/jsonviewer/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..7b92899918
--- /dev/null
+++ b/library/jsonviewer/src/main/res/values/colors.xml
@@ -0,0 +1,11 @@
+
+
+
+ #FF006700
+ #FF040091
+ #FF980000
+ #FF1700FF
+ #FF000000
+ #FFAAAAAA
+
+
diff --git a/library/jsonviewer/src/main/res/values/strings.xml b/library/jsonviewer/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..cc4b8726b4
--- /dev/null
+++ b/library/jsonviewer/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Copy Value
+
diff --git a/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt b/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt
new file mode 100644
index 0000000000..350bcdf289
--- /dev/null
+++ b/library/jsonviewer/src/test/java/org/billcarsonfr/jsonviewer/ModelParseTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2022 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 org.billcarsonfr.jsonviewer
+
+import org.junit.Assert
+import org.junit.Test
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ModelParseTest {
+ @Test
+ fun parsing_isCorrect() {
+ val string = """
+ {
+ "glossary": {
+ "title": "example glossary",
+ "GlossDiv": {
+ "title": "S",
+ "GlossList": {
+ "GlossEntry": {
+ "ID": "SGML",
+ "SortAs": "SGML",
+ "GlossTerm": "Standard Generalized Markup Language",
+ "Acronym": "SGML",
+ "Abbrev": "ISO 8879:1986",
+ "GlossDef": {
+ "para": "A meta-markup language, used to create markup languages such as DocBook.",
+ "GlossSeeAlso": ["GML", "XML"]
+ },
+ "GlossSee": "markup"
+ }
+ }
+ }
+ }
+ }
+ """.trim()
+
+ val model = ModelParser.fromJsonString(string)
+
+ Assert.assertEquals(0, model.depth)
+ Assert.assertEquals(1, model.keys.size)
+ Assert.assertTrue(model.keys.containsKey("glossary"))
+ Assert.assertTrue(model.keys["glossary"] is JSonViewerObject)
+
+ val glossary = model.keys["glossary"] as JSonViewerObject
+ Assert.assertEquals(2, glossary.keys.size)
+ Assert.assertTrue(glossary.keys.containsKey("title"))
+ Assert.assertTrue(glossary.keys.containsKey("GlossDiv"))
+
+ Assert.assertTrue(glossary.keys["title"] is JSonViewerLeaf)
+ (glossary.keys["title"] as JSonViewerLeaf).let {
+ Assert.assertEquals(JSONType.STRING, it.type)
+ }
+
+ Assert.assertTrue(glossary.keys["GlossDiv"] is JSonViewerObject)
+ val glossDiv = glossary.keys["GlossDiv"] as JSonViewerObject
+
+ Assert.assertTrue(glossDiv.keys["GlossList"] is JSonViewerObject)
+ val glossList = glossDiv.keys["GlossList"] as JSonViewerObject
+
+ Assert.assertTrue(glossList.keys["GlossEntry"] is JSonViewerObject)
+ val glossEntry = glossList.keys["GlossEntry"] as JSonViewerObject
+
+ Assert.assertTrue(glossEntry.keys["GlossDef"] is JSonViewerObject)
+ val glossDef = glossEntry.keys["GlossDef"] as JSonViewerObject
+
+ Assert.assertTrue(glossDef.keys["GlossSeeAlso"] is JSonViewerArray)
+ val glossSeeAlso = glossDef.keys["GlossSeeAlso"] as JSonViewerArray
+
+ Assert.assertEquals(2, glossSeeAlso.items.size)
+ Assert.assertEquals(0, glossSeeAlso.items.first().index)
+ Assert.assertNull(glossSeeAlso.items.first().key)
+ Assert.assertEquals("GML", (glossSeeAlso.items.first() as JSonViewerLeaf).stringRes)
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 7d3c1de746..d3b217c517 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -5,4 +5,5 @@ include ':attachment-viewer'
include ':multipicker'
include ':library:core-utils'
include ':library:ui-styles'
+include ':library:jsonviewer'
include ':matrix-sdk-android-flow'
diff --git a/vector/build.gradle b/vector/build.gradle
index 96e85521ee..9203a8ec2f 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -332,6 +332,7 @@ dependencies {
implementation project(":diff-match-patch")
implementation project(":multipicker")
implementation project(":attachment-viewer")
+ implementation project(":library:jsonviewer")
implementation project(":library:ui-styles")
implementation project(":library:core-utils")
implementation 'androidx.multidex:multidex:2.0.1'
@@ -465,7 +466,6 @@ dependencies {
gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
implementation "androidx.emoji2:emoji2:1.0.1"
- implementation('com.github.BillCarsonFr:JsonViewer:0.7')
// WebRTC
// org.webrtc:google-webrtc is for development purposes only
diff --git a/vector/lint.xml b/vector/lint.xml
index 818349da24..f02090489c 100644
--- a/vector/lint.xml
+++ b/vector/lint.xml
@@ -40,6 +40,7 @@
+
@@ -82,10 +83,6 @@
-
-
-
-
diff --git a/vector/src/main/java/im/vector/app/core/utils/compat/MutableCollectionCompat.kt b/vector/src/main/java/im/vector/app/core/utils/compat/MutableCollectionCompat.kt
new file mode 100644
index 0000000000..e131b5f328
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/utils/compat/MutableCollectionCompat.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022 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.app.core.utils.compat
+
+import android.os.Build
+
+fun MutableCollection.removeIfCompat(predicate: (E) -> Boolean) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ removeIf(predicate)
+ } else {
+ removeAll(filter(predicate).toSet())
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt
index eb2a99fdd4..d3c1767c83 100644
--- a/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt
+++ b/vector/src/main/java/im/vector/app/features/analytics/DecryptionFailureTracker.kt
@@ -17,6 +17,7 @@
package im.vector.app.features.analytics
import im.vector.app.core.time.Clock
+import im.vector.app.core.utils.compat.removeIfCompat
import im.vector.app.features.analytics.plan.Error
import im.vector.lib.core.utils.flow.tickerFlow
import kotlinx.coroutines.CoroutineScope
@@ -89,7 +90,7 @@ class DecryptionFailureTracker @Inject constructor(
fun onTimeLineDisposed(roomId: String) {
scope.launch(Dispatchers.Default) {
synchronized(failures) {
- failures.removeIf { it.roomId == roomId }
+ failures.removeIfCompat { it.roomId == roomId }
}
}
}
@@ -105,7 +106,7 @@ class DecryptionFailureTracker @Inject constructor(
private fun removeFailureForEventId(eventId: String) {
synchronized(failures) {
- failures.removeIf { it.failedEventId == eventId }
+ failures.removeIfCompat { it.failedEventId == eventId }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index 241ccb7428..4a9a03789f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -72,6 +72,7 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import timber.log.Timber
import javax.inject.Inject
+import kotlin.math.min
import kotlin.system.measureTimeMillis
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
@@ -185,6 +186,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
override fun onChanged(position: Int, count: Int, payload: Any?) {
synchronized(modelCache) {
assertUpdateCallbacksAllowed()
+ Timber.v("listUpdateCallback.onChanged(position: $position, count: $count). " +
+ "\ncurrentSnapshot has size of ${currentSnapshot.size} items")
(position until position + count).forEach {
// Invalidate cache
modelCache[it] = null
@@ -192,10 +195,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
// Also invalidate the first previous displayable event if
// it's sent by the same user so we are sure we have up to date information.
val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
- val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast {
+ // In some cases onChanged will be called before onRemoved and onInserted so position will be bigger than currentSnapshot.size.
+ val prevList = currentSnapshot.subList(0, min(position, currentSnapshot.size))
+ val prevDisplayableEventIndex = prevList.indexOfLast {
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
}
- if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) {
+ if (prevDisplayableEventIndex != -1 && currentSnapshot.getOrNull(prevDisplayableEventIndex)?.senderInfo?.userId == invalidatedSenderId) {
modelCache[prevDisplayableEventIndex] = null
}
requestModelBuild()
@@ -205,6 +210,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
override fun onMoved(fromPosition: Int, toPosition: Int) {
synchronized(modelCache) {
assertUpdateCallbacksAllowed()
+ Timber.v("listUpdateCallback.onMoved(fromPosition: $fromPosition, toPosition: $toPosition). " +
+ "\ncurrentSnapshot has size of ${currentSnapshot.size} items")
val model = modelCache.removeAt(fromPosition)
modelCache.add(toPosition, model)
requestModelBuild()
@@ -214,6 +221,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
override fun onInserted(position: Int, count: Int) {
synchronized(modelCache) {
assertUpdateCallbacksAllowed()
+ Timber.v("listUpdateCallback.onInserted(position: $position, count: $count). " +
+ "\ncurrentSnapshot has size of ${currentSnapshot.size} items")
repeat(count) {
modelCache.add(position, null)
}
@@ -224,6 +233,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
override fun onRemoved(position: Int, count: Int) {
synchronized(modelCache) {
assertUpdateCallbacksAllowed()
+ Timber.v("listUpdateCallback.onRemoved(position: $position, count: $count). " +
+ "\ncurrentSnapshot has size of ${currentSnapshot.size} items")
repeat(count) {
modelCache.removeAt(position)
}
@@ -306,6 +317,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
inSubmitList = true
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
currentSnapshot = newSnapshot
+ Timber.v("Submit a new snapshot of ${currentSnapshot.size} items.")
val diffResult = DiffUtil.calculateDiff(diffCallback)
diffResult.dispatchUpdatesTo(listUpdateCallback)
requestDelayedModelBuild(0)
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
index 3ae0ecccd2..366ad044b3 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/OnboardingViewModel.kt
@@ -852,13 +852,17 @@ class OnboardingViewModel @AssistedInject constructor(
}
withState {
- when (it.onboardingFlow) {
- OnboardingFlow.SignIn -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignIn))
- OnboardingFlow.SignUp -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignUp))
- OnboardingFlow.SignInSignUp,
- null -> {
- _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved)
+ if (it.serverType == ServerType.MatrixOrg) {
+ when (it.onboardingFlow) {
+ OnboardingFlow.SignIn -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignIn))
+ OnboardingFlow.SignUp -> handleUpdateSignMode(OnboardingAction.UpdateSignMode(SignMode.SignUp))
+ OnboardingFlow.SignInSignUp,
+ null -> {
+ _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved)
+ }
}
+ } else {
+ _viewEvents.post(OnboardingViewEvents.OnLoginFlowRetrieved)
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt
index d64ee0f705..06d8a0bf88 100644
--- a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt
+++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt
@@ -31,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import im.vector.app.R
+import im.vector.app.core.utils.compat.removeIfCompat
import im.vector.app.features.reactions.data.EmojiData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -215,14 +216,7 @@ class EmojiRecyclerAdapter @Inject constructor() :
override fun onViewRecycled(holder: ViewHolder) {
if (holder is EmojiViewHolder) {
holder.data = null
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- toUpdateWhenNotBusy.removeIf { it.second == holder }
- } else {
- val index = toUpdateWhenNotBusy.indexOfFirst { it.second == holder }
- if (index != -1) {
- toUpdateWhenNotBusy.removeAt(index)
- }
- }
+ toUpdateWhenNotBusy.removeIfCompat { it.second == holder }
}
super.onViewRecycled(holder)
}