mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 10:25:35 +03:00
Merge pull request #3502 from vector-im/feature/bca/spaces_dnd
Feature/bca/spaces dnd
This commit is contained in:
commit
5325c761f4
17 changed files with 829 additions and 38 deletions
|
@ -0,0 +1,260 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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.matrix.android.sdk
|
||||||
|
|
||||||
|
import org.amshove.kluent.internal.assertEquals
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.session.space.SpaceOrderUtils
|
||||||
|
|
||||||
|
class SpaceOrderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOrderBetweenNodesWithOrder() {
|
||||||
|
val orderedSpaces = listOf(
|
||||||
|
"roomId1" to "a",
|
||||||
|
"roomId2" to "m",
|
||||||
|
"roomId3" to "z"
|
||||||
|
).assertSpaceOrdered()
|
||||||
|
|
||||||
|
val orderCommand = SpaceOrderUtils.orderCommandsForMove(orderedSpaces, "roomId1", 1)
|
||||||
|
|
||||||
|
Assert.assertTrue("Only one order should be changed", orderCommand.size == 1)
|
||||||
|
Assert.assertTrue("Moved space order should change", orderCommand.first().spaceId == "roomId1")
|
||||||
|
|
||||||
|
Assert.assertTrue("m" < orderCommand[0].order)
|
||||||
|
Assert.assertTrue(orderCommand[0].order < "z")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMoveLastBetweenNodesWithOrder() {
|
||||||
|
val orderedSpaces = listOf(
|
||||||
|
"roomId1" to "a",
|
||||||
|
"roomId2" to "m",
|
||||||
|
"roomId3" to "z"
|
||||||
|
).assertSpaceOrdered()
|
||||||
|
|
||||||
|
val orderCommand = SpaceOrderUtils.orderCommandsForMove(orderedSpaces, "roomId1", 2)
|
||||||
|
|
||||||
|
Assert.assertTrue("Only one order should be changed", orderCommand.size == 1)
|
||||||
|
Assert.assertTrue("Moved space order should change", orderCommand.first().spaceId == "roomId1")
|
||||||
|
|
||||||
|
Assert.assertTrue("z" < orderCommand[0].order)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMoveUpNoOrder() {
|
||||||
|
val orderedSpaces = listOf(
|
||||||
|
"roomId1" to null,
|
||||||
|
"roomId2" to null,
|
||||||
|
"roomId3" to null
|
||||||
|
).assertSpaceOrdered()
|
||||||
|
|
||||||
|
val orderCommand = SpaceOrderUtils.orderCommandsForMove(orderedSpaces, "roomId1", 1)
|
||||||
|
|
||||||
|
Assert.assertTrue("2 orders change should be needed", orderCommand.size == 2)
|
||||||
|
|
||||||
|
val reOrdered = reOrderWithCommands(orderedSpaces, orderCommand)
|
||||||
|
|
||||||
|
Assert.assertEquals("roomId2", reOrdered[0].first)
|
||||||
|
Assert.assertEquals("roomId1", reOrdered[1].first)
|
||||||
|
Assert.assertEquals("roomId3", reOrdered[2].first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMoveUpNotEnoughSpace() {
|
||||||
|
val orderedSpaces = listOf(
|
||||||
|
"roomId1" to "a",
|
||||||
|
"roomId2" to "j",
|
||||||
|
"roomId3" to "k"
|
||||||
|
).assertSpaceOrdered()
|
||||||
|
|
||||||
|
val orderCommand = SpaceOrderUtils.orderCommandsForMove(orderedSpaces, "roomId1", 1)
|
||||||
|
|
||||||
|
Assert.assertTrue("more orders change should be needed", orderCommand.size > 1)
|
||||||
|
|
||||||
|
val reOrdered = reOrderWithCommands(orderedSpaces, orderCommand)
|
||||||
|
|
||||||
|
Assert.assertEquals("roomId2", reOrdered[0].first)
|
||||||
|
Assert.assertEquals("roomId1", reOrdered[1].first)
|
||||||
|
Assert.assertEquals("roomId3", reOrdered[2].first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMoveEndNoOrder() {
|
||||||
|
val orderedSpaces = listOf(
|
||||||
|
"roomId1" to null,
|
||||||
|
"roomId2" to null,
|
||||||
|
"roomId3" to null
|
||||||
|
).assertSpaceOrdered()
|
||||||
|
|
||||||
|
val orderCommand = SpaceOrderUtils.orderCommandsForMove(orderedSpaces, "roomId1", 2)
|
||||||
|
|
||||||
|
// Actually 2 could be enough... as it's last it can stays with null
|
||||||
|
Assert.assertEquals("3 orders change should be needed", 3, orderCommand.size)
|
||||||
|
|
||||||
|
val reOrdered = reOrderWithCommands(orderedSpaces, orderCommand)
|
||||||
|
|
||||||
|
Assert.assertEquals("roomId2", reOrdered[0].first)
|
||||||
|
Assert.assertEquals("roomId3", reOrdered[1].first)
|
||||||
|
Assert.assertEquals("roomId1", reOrdered[2].first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMoveUpBiggerOrder() {
|
||||||
|
val orderedSpaces = listOf(
|
||||||
|
"roomId1" to "aaaa",
|
||||||
|
"roomId2" to "ffff",
|
||||||
|
"roomId3" to "pppp",
|
||||||
|
"roomId4" to null,
|
||||||
|
"roomId5" to null,
|
||||||
|
"roomId6" to null
|
||||||
|
).assertSpaceOrdered()
|
||||||
|
|
||||||
|
val orderCommand = SpaceOrderUtils.orderCommandsForMove(orderedSpaces, "roomId2", 3)
|
||||||
|
|
||||||
|
// Actually 2 could be enough... as it's last it can stays with null
|
||||||
|
Assert.assertEquals("3 orders change should be needed", 3, orderCommand.size)
|
||||||
|
|
||||||
|
val reOrdered = reOrderWithCommands(orderedSpaces, orderCommand)
|
||||||
|
|
||||||
|
Assert.assertEquals("roomId1", reOrdered[0].first)
|
||||||
|
Assert.assertEquals("roomId3", reOrdered[1].first)
|
||||||
|
Assert.assertEquals("roomId4", reOrdered[2].first)
|
||||||
|
Assert.assertEquals("roomId5", reOrdered[3].first)
|
||||||
|
Assert.assertEquals("roomId2", reOrdered[4].first)
|
||||||
|
Assert.assertEquals("roomId6", reOrdered[5].first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMoveDownBetweenNodesWithOrder() {
|
||||||
|
val orderedSpaces = listOf(
|
||||||
|
"roomId1" to "a",
|
||||||
|
"roomId2" to "m",
|
||||||
|
"roomId3" to "z"
|
||||||
|
).assertSpaceOrdered()
|
||||||
|
|
||||||
|
val orderCommand = SpaceOrderUtils.orderCommandsForMove(orderedSpaces, "roomId3", -1)
|
||||||
|
|
||||||
|
Assert.assertTrue("Only one order should be changed", orderCommand.size == 1)
|
||||||
|
Assert.assertTrue("Moved space order should change", orderCommand.first().spaceId == "roomId3")
|
||||||
|
|
||||||
|
val reOrdered = reOrderWithCommands(orderedSpaces, orderCommand)
|
||||||
|
|
||||||
|
Assert.assertEquals("roomId1", reOrdered[0].first)
|
||||||
|
Assert.assertEquals("roomId3", reOrdered[1].first)
|
||||||
|
Assert.assertEquals("roomId2", reOrdered[2].first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMoveDownNoOrder() {
|
||||||
|
val orderedSpaces = listOf(
|
||||||
|
"roomId1" to null,
|
||||||
|
"roomId2" to null,
|
||||||
|
"roomId3" to null
|
||||||
|
).assertSpaceOrdered()
|
||||||
|
|
||||||
|
val orderCommand = SpaceOrderUtils.orderCommandsForMove(orderedSpaces, "roomId3", -1)
|
||||||
|
|
||||||
|
Assert.assertTrue("2 orders change should be needed", orderCommand.size == 2)
|
||||||
|
|
||||||
|
val reOrdered = reOrderWithCommands(orderedSpaces, orderCommand)
|
||||||
|
|
||||||
|
Assert.assertEquals("roomId1", reOrdered[0].first)
|
||||||
|
Assert.assertEquals("roomId3", reOrdered[1].first)
|
||||||
|
Assert.assertEquals("roomId2", reOrdered[2].first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMoveDownBiggerOrder() {
|
||||||
|
val orderedSpaces = listOf(
|
||||||
|
"roomId1" to "aaaa",
|
||||||
|
"roomId2" to "ffff",
|
||||||
|
"roomId3" to "pppp",
|
||||||
|
"roomId4" to null,
|
||||||
|
"roomId5" to null,
|
||||||
|
"roomId6" to null
|
||||||
|
).assertSpaceOrdered()
|
||||||
|
|
||||||
|
val orderCommand = SpaceOrderUtils.orderCommandsForMove(orderedSpaces, "roomId5", -4)
|
||||||
|
|
||||||
|
Assert.assertEquals("1 order change should be needed", 1, orderCommand.size)
|
||||||
|
|
||||||
|
val reOrdered = reOrderWithCommands(orderedSpaces, orderCommand)
|
||||||
|
|
||||||
|
Assert.assertEquals("roomId5", reOrdered[0].first)
|
||||||
|
Assert.assertEquals("roomId1", reOrdered[1].first)
|
||||||
|
Assert.assertEquals("roomId2", reOrdered[2].first)
|
||||||
|
Assert.assertEquals("roomId3", reOrdered[3].first)
|
||||||
|
Assert.assertEquals("roomId4", reOrdered[4].first)
|
||||||
|
Assert.assertEquals("roomId6", reOrdered[5].first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMultipleMoveOrder() {
|
||||||
|
val orderedSpaces = listOf(
|
||||||
|
"roomId1" to null,
|
||||||
|
"roomId2" to null,
|
||||||
|
"roomId3" to null,
|
||||||
|
"roomId4" to null,
|
||||||
|
"roomId5" to null,
|
||||||
|
"roomId6" to null
|
||||||
|
).assertSpaceOrdered()
|
||||||
|
|
||||||
|
// move 5 to Top
|
||||||
|
val fiveToTop = SpaceOrderUtils.orderCommandsForMove(orderedSpaces, "roomId5", -4)
|
||||||
|
|
||||||
|
val fiveTopReOrder = reOrderWithCommands(orderedSpaces, fiveToTop)
|
||||||
|
|
||||||
|
// now move 4 to second
|
||||||
|
val orderCommand = SpaceOrderUtils.orderCommandsForMove(fiveTopReOrder, "roomId4", -3)
|
||||||
|
|
||||||
|
val reOrdered = reOrderWithCommands(fiveTopReOrder, orderCommand)
|
||||||
|
// second order should cost 1 re-order
|
||||||
|
Assert.assertEquals(1, orderCommand.size)
|
||||||
|
|
||||||
|
Assert.assertEquals("roomId5", reOrdered[0].first)
|
||||||
|
Assert.assertEquals("roomId4", reOrdered[1].first)
|
||||||
|
Assert.assertEquals("roomId1", reOrdered[2].first)
|
||||||
|
Assert.assertEquals("roomId2", reOrdered[3].first)
|
||||||
|
Assert.assertEquals("roomId3", reOrdered[4].first)
|
||||||
|
Assert.assertEquals("roomId6", reOrdered[5].first)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testComparator() {
|
||||||
|
listOf(
|
||||||
|
"roomId2" to "a",
|
||||||
|
"roomId1" to "b",
|
||||||
|
"roomId3" to null,
|
||||||
|
"roomId4" to null
|
||||||
|
).assertSpaceOrdered()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reOrderWithCommands(orderedSpaces: List<Pair<String, String?>>, orderCommand: List<SpaceOrderUtils.SpaceReOrderCommand>) =
|
||||||
|
orderedSpaces.map { orderInfo ->
|
||||||
|
orderInfo.first to (orderCommand.find { it.spaceId == orderInfo.first }?.order ?: orderInfo.second)
|
||||||
|
}
|
||||||
|
.sortedWith(testSpaceComparator)
|
||||||
|
|
||||||
|
private fun List<Pair<String, String?>>.assertSpaceOrdered(): List<Pair<String, String?>> {
|
||||||
|
assertEquals(this, this.sortedWith(testSpaceComparator))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private val testSpaceComparator = compareBy<Pair<String, String?>, String?>(nullsLast()) { it.second }.thenBy { it.first }
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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.matrix.android.sdk
|
||||||
|
|
||||||
|
import org.amshove.kluent.internal.assertEquals
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.MatrixPatterns
|
||||||
|
import org.matrix.android.sdk.api.util.StringOrderUtils
|
||||||
|
|
||||||
|
class StringOrderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testbasing() {
|
||||||
|
assertEquals("a", StringOrderUtils.baseToString(StringOrderUtils.stringToBase("a", StringOrderUtils.DEFAULT_ALPHABET), StringOrderUtils.DEFAULT_ALPHABET))
|
||||||
|
assertEquals("element", StringOrderUtils.baseToString(StringOrderUtils.stringToBase("element", StringOrderUtils.DEFAULT_ALPHABET), StringOrderUtils.DEFAULT_ALPHABET))
|
||||||
|
assertEquals("matrix", StringOrderUtils.baseToString(StringOrderUtils.stringToBase("matrix", StringOrderUtils.DEFAULT_ALPHABET), StringOrderUtils.DEFAULT_ALPHABET))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testValid() {
|
||||||
|
println(StringOrderUtils.DEFAULT_ALPHABET.joinToString(","))
|
||||||
|
|
||||||
|
assert(MatrixPatterns.isValidOrderString("a"))
|
||||||
|
assert(MatrixPatterns.isValidOrderString(" "))
|
||||||
|
assert(MatrixPatterns.isValidOrderString("abc"))
|
||||||
|
assert(!MatrixPatterns.isValidOrderString("abcê"))
|
||||||
|
assert(!MatrixPatterns.isValidOrderString(""))
|
||||||
|
assert(MatrixPatterns.isValidOrderString("!"))
|
||||||
|
assert(MatrixPatterns.isValidOrderString("!\"#\$%&'()*+,012"))
|
||||||
|
assert(!MatrixPatterns.isValidOrderString(Char(' '.code - 1).toString()))
|
||||||
|
|
||||||
|
assert(!MatrixPatterns.isValidOrderString(
|
||||||
|
buildString {
|
||||||
|
for (i in 0..49) {
|
||||||
|
append(StringOrderUtils.DEFAULT_ALPHABET.random())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
assert(MatrixPatterns.isValidOrderString(
|
||||||
|
buildString {
|
||||||
|
for (i in 0..48) {
|
||||||
|
append(StringOrderUtils.DEFAULT_ALPHABET.random())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAverage() {
|
||||||
|
assertAverage("${StringOrderUtils.DEFAULT_ALPHABET.first()}", "m")
|
||||||
|
assertAverage("aa", "aab")
|
||||||
|
assertAverage("matrix", "element")
|
||||||
|
assertAverage("mmm", "mmmmm")
|
||||||
|
assertAverage("aab", "aa")
|
||||||
|
assertAverage("", "aa")
|
||||||
|
assertAverage("a", "z")
|
||||||
|
assertAverage("ground", "sky")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMidPoints() {
|
||||||
|
val orders = StringOrderUtils.midPoints("element", "matrix", 4)
|
||||||
|
assertEquals(4, orders!!.size)
|
||||||
|
assert("element" < orders[0])
|
||||||
|
assert(orders[0] < orders[1])
|
||||||
|
assert(orders[1] < orders[2])
|
||||||
|
assert(orders[2] < orders[3])
|
||||||
|
assert(orders[3] < "matrix")
|
||||||
|
|
||||||
|
println("element < ${orders.joinToString(" < ") { "[$it]" }} < matrix")
|
||||||
|
|
||||||
|
val orders2 = StringOrderUtils.midPoints("a", "d", 4)
|
||||||
|
assertEquals(null, orders2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRenumberNeeded() {
|
||||||
|
assertEquals(null, StringOrderUtils.average("a", "a"))
|
||||||
|
assertEquals(null, StringOrderUtils.average("", ""))
|
||||||
|
assertEquals(null, StringOrderUtils.average("a", "b"))
|
||||||
|
assertEquals(null, StringOrderUtils.average("b", "a"))
|
||||||
|
assertEquals(null, StringOrderUtils.average("mmmm", "mmmm"))
|
||||||
|
assertEquals(null, StringOrderUtils.average("a${Char(0)}", "a"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertAverage(first: String, second: String) {
|
||||||
|
val left = first.takeIf { first < second } ?: second
|
||||||
|
val right = first.takeIf { first > second } ?: second
|
||||||
|
val av1 = StringOrderUtils.average(left, right)!!
|
||||||
|
println("[$left] < [$av1] < [$right]")
|
||||||
|
Assert.assertTrue(left < av1)
|
||||||
|
Assert.assertTrue(av1 < right)
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,6 +71,9 @@ object MatrixPatterns {
|
||||||
private const val LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
|
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 = LINK_TO_APP_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE)
|
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = LINK_TO_APP_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE)
|
||||||
|
|
||||||
|
// ascii characters in the range \x20 (space) to \x7E (~)
|
||||||
|
val ORDER_STRING_REGEX = "[ -~]+".toRegex()
|
||||||
|
|
||||||
// list of patterns to find some matrix item.
|
// list of patterns to find some matrix item.
|
||||||
val MATRIX_PATTERNS = listOf(
|
val MATRIX_PATTERNS = listOf(
|
||||||
PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID,
|
PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID,
|
||||||
|
@ -146,4 +149,12 @@ object MatrixPatterns {
|
||||||
fun extractServerNameFromId(matrixId: String?): String? {
|
fun extractServerNameFromId(matrixId: String?): String? {
|
||||||
return matrixId?.substringAfter(":", missingDelimiterValue = "")?.takeIf { it.isNotEmpty() }
|
return matrixId?.substringAfter(":", missingDelimiterValue = "")?.takeIf { it.isNotEmpty() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7E (~),
|
||||||
|
* or consist of more than 50 characters, are forbidden and the field should be ignored if received.
|
||||||
|
*/
|
||||||
|
fun isValidOrderString(order: String?) : Boolean {
|
||||||
|
return order != null && order.length < 50 && order matches ORDER_STRING_REGEX
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,4 +20,5 @@ object RoomAccountDataTypes {
|
||||||
const val EVENT_TYPE_VIRTUAL_ROOM = "im.vector.is_virtual_room"
|
const val EVENT_TYPE_VIRTUAL_ROOM = "im.vector.is_virtual_room"
|
||||||
const val EVENT_TYPE_TAG = "m.tag"
|
const val EVENT_TYPE_TAG = "m.tag"
|
||||||
const val EVENT_TYPE_FULLY_READ = "m.fully_read"
|
const val EVENT_TYPE_FULLY_READ = "m.fully_read"
|
||||||
|
const val EVENT_TYPE_SPACE_ORDER = "org.matrix.msc3230.space_order" // m.space_order
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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.matrix.android.sdk.api.session.space
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.util.StringOrderUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds some utilities to compute correct string orders when ordering spaces.
|
||||||
|
* After moving a space (e.g via DnD), client should limit the number of room account data update.
|
||||||
|
* For example if the space is moved between two other spaces with orders, just update the moved space order by computing
|
||||||
|
* a mid point between the surrounding orders.
|
||||||
|
* If the space is moved after a space with no order, all the previous spaces should be then ordered,
|
||||||
|
* and the computed orders should be chosen so that there is enough gaps in between them to facilitate future re-order.
|
||||||
|
* Re numbering (i.e change all spaces m.space.order account data) should be avoided as much as possible,
|
||||||
|
* as the updates might not be atomic for other clients and would makes spaces jump around.
|
||||||
|
*/
|
||||||
|
object SpaceOrderUtils {
|
||||||
|
|
||||||
|
data class SpaceReOrderCommand(
|
||||||
|
val spaceId: String,
|
||||||
|
val order: String
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a minimal list of order change in order to re order the space list as per given move.
|
||||||
|
*/
|
||||||
|
fun orderCommandsForMove(orderedSpacesToOrderMap: List<Pair<String, String?>>, movedSpaceId: String, delta: Int): List<SpaceReOrderCommand> {
|
||||||
|
val movedIndex = orderedSpacesToOrderMap.indexOfFirst { it.first == movedSpaceId }
|
||||||
|
if (movedIndex == -1) return emptyList()
|
||||||
|
if (delta == 0) return emptyList()
|
||||||
|
|
||||||
|
val targetIndex = if (delta > 0) movedIndex + delta else (movedIndex + delta - 1)
|
||||||
|
|
||||||
|
val nodesToReNumber = mutableListOf<String>()
|
||||||
|
var lowerBondOrder: String? = null
|
||||||
|
var index = targetIndex
|
||||||
|
while (index >= 0 && lowerBondOrder == null) {
|
||||||
|
val node = orderedSpacesToOrderMap.getOrNull(index)
|
||||||
|
if (node != null /*null when adding at the end*/) {
|
||||||
|
val nodeOrder = node.second
|
||||||
|
if (node.first == movedSpaceId) break
|
||||||
|
if (nodeOrder == null) {
|
||||||
|
nodesToReNumber.add(0, node.first)
|
||||||
|
} else {
|
||||||
|
lowerBondOrder = nodeOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index--
|
||||||
|
}
|
||||||
|
nodesToReNumber.add(movedSpaceId)
|
||||||
|
val afterSpace: Pair<String, String?>? = if (orderedSpacesToOrderMap.indices.contains(targetIndex + 1)) {
|
||||||
|
orderedSpacesToOrderMap[targetIndex + 1]
|
||||||
|
} else null
|
||||||
|
|
||||||
|
val defaultMaxOrder = CharArray(4) { StringOrderUtils.DEFAULT_ALPHABET.last() }
|
||||||
|
.joinToString("")
|
||||||
|
|
||||||
|
val defaultMinOrder = CharArray(4) { StringOrderUtils.DEFAULT_ALPHABET.first() }
|
||||||
|
.joinToString("")
|
||||||
|
|
||||||
|
val afterOrder = afterSpace?.second ?: defaultMaxOrder
|
||||||
|
|
||||||
|
val beforeOrder = lowerBondOrder ?: defaultMinOrder
|
||||||
|
|
||||||
|
val newOrder = StringOrderUtils.midPoints(beforeOrder, afterOrder, nodesToReNumber.size)
|
||||||
|
|
||||||
|
if (newOrder.isNullOrEmpty()) {
|
||||||
|
// re order all?
|
||||||
|
val expectedList = orderedSpacesToOrderMap.toMutableList()
|
||||||
|
expectedList.removeAt(movedIndex).let {
|
||||||
|
expectedList.add(movedIndex + delta, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return StringOrderUtils.midPoints(defaultMinOrder, defaultMaxOrder, orderedSpacesToOrderMap.size)?.let { orders ->
|
||||||
|
expectedList.mapIndexed { index, pair ->
|
||||||
|
SpaceReOrderCommand(
|
||||||
|
pair.first,
|
||||||
|
orders[index]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
} else {
|
||||||
|
return nodesToReNumber.mapIndexed { i, s ->
|
||||||
|
SpaceReOrderCommand(
|
||||||
|
s,
|
||||||
|
newOrder[i]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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.matrix.android.sdk.api.session.space.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import org.matrix.android.sdk.api.MatrixPatterns
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {
|
||||||
|
* "type": "m.space_order",
|
||||||
|
* "content": {
|
||||||
|
* "order": "..."
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class SpaceOrderContent(
|
||||||
|
val order: String? = null
|
||||||
|
) {
|
||||||
|
fun safeOrder(): String? {
|
||||||
|
return order?.takeIf { MatrixPatterns.isValidOrderString(it) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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.matrix.android.sdk.api.session.space.model
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
|
|
||||||
|
// Can't use regular compare by because Null is considered less than any value, and for space order it's the opposite
|
||||||
|
class TopLevelSpaceComparator(val orders: Map<String, String?>) : Comparator<RoomSummary> {
|
||||||
|
|
||||||
|
override fun compare(left: RoomSummary?, right: RoomSummary?): Int {
|
||||||
|
val leftOrder = left?.roomId?.let { orders[it] }
|
||||||
|
val rightOrder = right?.roomId?.let { orders[it] }
|
||||||
|
return if (leftOrder != null && rightOrder != null) {
|
||||||
|
leftOrder.compareTo(rightOrder)
|
||||||
|
} else {
|
||||||
|
if (leftOrder == null) {
|
||||||
|
if (rightOrder == null) {
|
||||||
|
compareValues(left?.roomId, right?.roomId)
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// .also {
|
||||||
|
// Timber.w("VAL: compare(${left?.displayName} | $leftOrder ,${right?.displayName} | $rightOrder) = $it")
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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.matrix.android.sdk.api.util
|
||||||
|
|
||||||
|
import java.math.BigInteger
|
||||||
|
|
||||||
|
object StringOrderUtils {
|
||||||
|
|
||||||
|
val DEFAULT_ALPHABET = buildString {
|
||||||
|
for (i in 0x20..0x7E) {
|
||||||
|
append(Char(i))
|
||||||
|
}
|
||||||
|
}.toCharArray()
|
||||||
|
|
||||||
|
// /=Range(0x20, 0x7E)
|
||||||
|
|
||||||
|
fun average(left: String, right: String, alphabet: CharArray = DEFAULT_ALPHABET): String? {
|
||||||
|
return midPoints(left, right, 1, alphabet)?.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun midPoints(left: String, right: String, count: Int, alphabet: CharArray = DEFAULT_ALPHABET): List<String>? {
|
||||||
|
if (left == right) return null // no space in between..
|
||||||
|
if (left > right) return midPoints(right, left, count, alphabet)
|
||||||
|
val size = maxOf(left.length, right.length)
|
||||||
|
val leftPadded = pad(left, size, alphabet.first())
|
||||||
|
val rightPadded = pad(right, size, alphabet.first())
|
||||||
|
val b1 = stringToBase(leftPadded, alphabet)
|
||||||
|
val b2 = stringToBase(rightPadded, alphabet)
|
||||||
|
val step = (b2.minus(b1)).div(BigInteger("${count + 1}"))
|
||||||
|
val orders = mutableListOf<String>()
|
||||||
|
var previous = left
|
||||||
|
for (i in 0 until count) {
|
||||||
|
val newOrder = baseToString(b1.add(step.multiply(BigInteger("${i + 1}"))), alphabet)
|
||||||
|
orders.add(newOrder)
|
||||||
|
// ensure there was enought precision
|
||||||
|
if (previous >= newOrder) return null
|
||||||
|
previous = newOrder
|
||||||
|
}
|
||||||
|
return orders.takeIf { orders.last() < right }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pad(string: String, size: Int, padding: Char): String {
|
||||||
|
val raw = string.toCharArray()
|
||||||
|
return CharArray(size).also {
|
||||||
|
for (index in it.indices) {
|
||||||
|
if (index < raw.size) {
|
||||||
|
it[index] = raw[index]
|
||||||
|
} else {
|
||||||
|
it[index] = padding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.joinToString("")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun baseToString(x: BigInteger, alphabet: CharArray): String {
|
||||||
|
val len = alphabet.size.toBigInteger()
|
||||||
|
if (x < len) {
|
||||||
|
return alphabet[x.toInt()].toString()
|
||||||
|
} else {
|
||||||
|
return baseToString(x.div(len), alphabet) + alphabet[x.rem(len).toInt()].toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stringToBase(x: String, alphabet: CharArray): BigInteger {
|
||||||
|
if (x.isEmpty()) throw IllegalArgumentException()
|
||||||
|
val len = alphabet.size.toBigInteger()
|
||||||
|
var result = BigInteger("0")
|
||||||
|
x.reversed().forEachIndexed { index, c ->
|
||||||
|
result += (alphabet.indexOf(c).toBigInteger() * len.pow(index))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,8 +26,8 @@ import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataServic
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
|
||||||
internal class DefaultRoomAccountDataService @AssistedInject constructor(@Assisted private val roomId: String,
|
internal class DefaultRoomAccountDataService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||||
private val dataSource: RoomAccountDataDataSource,
|
private val dataSource: RoomAccountDataDataSource,
|
||||||
private val updateRoomAccountDataTask: UpdateRoomAccountDataTask
|
private val updateRoomAccountDataTask: UpdateRoomAccountDataTask
|
||||||
) : RoomAccountDataService {
|
) : RoomAccountDataService {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
1
newsfragment/3501.feature
Normal file
1
newsfragment/3501.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
User defined top level spaces ordering (#3501)
|
|
@ -26,6 +26,9 @@ sealed class SpaceListAction : VectorViewModelAction {
|
||||||
data class LeaveSpace(val spaceSummary: RoomSummary) : SpaceListAction()
|
data class LeaveSpace(val spaceSummary: RoomSummary) : SpaceListAction()
|
||||||
data class ToggleExpand(val spaceSummary: RoomSummary) : SpaceListAction()
|
data class ToggleExpand(val spaceSummary: RoomSummary) : SpaceListAction()
|
||||||
object AddSpace : SpaceListAction()
|
object AddSpace : SpaceListAction()
|
||||||
|
data class MoveSpace(val spaceId: String, val delta : Int) : SpaceListAction()
|
||||||
|
data class OnStartDragging(val spaceId: String, val expanded: Boolean) : SpaceListAction()
|
||||||
|
data class OnEndDragging(val spaceId: String, val expanded: Boolean) : SpaceListAction()
|
||||||
|
|
||||||
data class SelectLegacyGroup(val groupSummary: GroupSummary?) : SpaceListAction()
|
data class SelectLegacyGroup(val groupSummary: GroupSummary?) : SpaceListAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,11 @@
|
||||||
package im.vector.app.features.spaces
|
package im.vector.app.features.spaces
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import com.airbnb.epoxy.EpoxyTouchHelper
|
||||||
import com.airbnb.mvrx.Incomplete
|
import com.airbnb.mvrx.Incomplete
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
@ -54,6 +56,53 @@ class SpaceListFragment @Inject constructor(
|
||||||
spaceController.callback = this
|
spaceController.callback = this
|
||||||
views.stateView.contentView = views.groupListView
|
views.stateView.contentView = views.groupListView
|
||||||
views.groupListView.configureWith(spaceController)
|
views.groupListView.configureWith(spaceController)
|
||||||
|
EpoxyTouchHelper.initDragging(spaceController)
|
||||||
|
.withRecyclerView(views.groupListView)
|
||||||
|
.forVerticalList()
|
||||||
|
.withTarget(SpaceSummaryItem::class.java)
|
||||||
|
.andCallbacks(object : EpoxyTouchHelper.DragCallbacks<SpaceSummaryItem>() {
|
||||||
|
var toPositionM: Int? = null
|
||||||
|
var fromPositionM: Int? = null
|
||||||
|
var initialElevation: Float? = null
|
||||||
|
|
||||||
|
override fun onDragStarted(model: SpaceSummaryItem?, itemView: View?, adapterPosition: Int) {
|
||||||
|
toPositionM = null
|
||||||
|
fromPositionM = null
|
||||||
|
model?.matrixItem?.id?.let {
|
||||||
|
viewModel.handle(SpaceListAction.OnStartDragging(it, model.expanded))
|
||||||
|
}
|
||||||
|
itemView?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
|
initialElevation = itemView?.elevation
|
||||||
|
itemView?.elevation = 6f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDragReleased(model: SpaceSummaryItem?, itemView: View?) {
|
||||||
|
// Timber.v("VAL: onModelMoved from $fromPositionM to $toPositionM ${model?.matrixItem?.getBestName()}")
|
||||||
|
if (toPositionM == null || fromPositionM == null) return
|
||||||
|
val movingSpace = model?.matrixItem?.id ?: return
|
||||||
|
viewModel.handle(SpaceListAction.MoveSpace(movingSpace, toPositionM!! - fromPositionM!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearView(model: SpaceSummaryItem?, itemView: View?) {
|
||||||
|
// Timber.v("VAL: clearView ${model?.matrixItem?.getBestName()}")
|
||||||
|
itemView?.elevation = initialElevation ?: 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onModelMoved(fromPosition: Int, toPosition: Int, modelBeingMoved: SpaceSummaryItem?, itemView: View?) {
|
||||||
|
// Timber.v("VAL: onModelMoved incremental from $fromPosition to $toPosition ${modelBeingMoved?.matrixItem?.getBestName()}")
|
||||||
|
if (fromPositionM == null) {
|
||||||
|
fromPositionM = fromPosition
|
||||||
|
}
|
||||||
|
toPositionM = toPosition
|
||||||
|
itemView?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isDragEnabledForModel(model: SpaceSummaryItem?): Boolean {
|
||||||
|
// Timber.v("VAL: isDragEnabledForModel ${model?.matrixItem?.getBestName()}")
|
||||||
|
return model?.canDrag == true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
viewModel.observeViewEvents {
|
viewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
is SpaceListViewEvents.OpenSpaceSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenSpacePreview(it.id))
|
is SpaceListViewEvents.OpenSpaceSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenSpacePreview(it.id))
|
||||||
|
@ -74,7 +123,7 @@ class SpaceListFragment @Inject constructor(
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
when (state.asyncSpaces) {
|
when (state.asyncSpaces) {
|
||||||
is Incomplete -> views.stateView.state = StateView.State.Loading
|
is Incomplete -> views.stateView.state = StateView.State.Loading
|
||||||
is Success -> views.stateView.state = StateView.State.Content
|
is Success -> views.stateView.state = StateView.State.Content
|
||||||
}
|
}
|
||||||
spaceController.update(state)
|
spaceController.update(state)
|
||||||
}
|
}
|
||||||
|
@ -86,6 +135,7 @@ class SpaceListFragment @Inject constructor(
|
||||||
override fun onSpaceInviteSelected(spaceSummary: RoomSummary) {
|
override fun onSpaceInviteSelected(spaceSummary: RoomSummary) {
|
||||||
viewModel.handle(SpaceListAction.OpenSpaceInvite(spaceSummary))
|
viewModel.handle(SpaceListAction.OpenSpaceInvite(spaceSummary))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSpaceSettings(spaceSummary: RoomSummary) {
|
override fun onSpaceSettings(spaceSummary: RoomSummary) {
|
||||||
sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(spaceSummary.roomId))
|
sharedActionViewModel.post(HomeActivitySharedAction.ShowSpaceSettings(spaceSummary.roomId))
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,9 @@ data class SpaceListViewState(
|
||||||
val myMxItem : Async<MatrixItem.UserItem> = Uninitialized,
|
val myMxItem : Async<MatrixItem.UserItem> = Uninitialized,
|
||||||
val asyncSpaces: Async<List<RoomSummary>> = Uninitialized,
|
val asyncSpaces: Async<List<RoomSummary>> = Uninitialized,
|
||||||
val selectedGroupingMethod: RoomGroupingMethod = RoomGroupingMethod.BySpace(null),
|
val selectedGroupingMethod: RoomGroupingMethod = RoomGroupingMethod.BySpace(null),
|
||||||
val rootSpaces: List<RoomSummary>? = null,
|
val rootSpacesOrdered: List<RoomSummary>? = null,
|
||||||
|
val spaceOrderInfo: Map<String, String?>? = null,
|
||||||
|
val spaceOrderLocalEchos: Map<String, String?>? = null,
|
||||||
val legacyGroups: List<GroupSummary>? = null,
|
val legacyGroups: List<GroupSummary>? = null,
|
||||||
val expandedStates: Map<String, Boolean> = emptyMap(),
|
val expandedStates: Map<String, Boolean> = emptyMap(),
|
||||||
val homeAggregateCount : RoomAggregateNotificationCount = RoomAggregateNotificationCount(0, 0)
|
val homeAggregateCount : RoomAggregateNotificationCount = RoomAggregateNotificationCount(0, 0)
|
||||||
|
|
|
@ -62,7 +62,7 @@ class SpaceSummaryController @Inject constructor(
|
||||||
buildGroupModels(
|
buildGroupModels(
|
||||||
nonNullViewState.asyncSpaces(),
|
nonNullViewState.asyncSpaces(),
|
||||||
nonNullViewState.selectedGroupingMethod,
|
nonNullViewState.selectedGroupingMethod,
|
||||||
nonNullViewState.rootSpaces,
|
nonNullViewState.rootSpacesOrdered,
|
||||||
nonNullViewState.expandedStates,
|
nonNullViewState.expandedStates,
|
||||||
nonNullViewState.homeAggregateCount)
|
nonNullViewState.homeAggregateCount)
|
||||||
|
|
||||||
|
@ -127,6 +127,7 @@ class SpaceSummaryController @Inject constructor(
|
||||||
countState(UnreadCounterBadgeView.State(1, true))
|
countState(UnreadCounterBadgeView.State(1, true))
|
||||||
selected(false)
|
selected(false)
|
||||||
description(host.stringProvider.getString(R.string.you_are_invited))
|
description(host.stringProvider.getString(R.string.you_are_invited))
|
||||||
|
canDrag(false)
|
||||||
listener { host.callback?.onSpaceInviteSelected(roomSummary) }
|
listener { host.callback?.onSpaceInviteSelected(roomSummary) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,7 +140,6 @@ class SpaceSummaryController @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
rootSpaces
|
rootSpaces
|
||||||
?.sortedBy { it.roomId }
|
|
||||||
?.forEach { groupSummary ->
|
?.forEach { groupSummary ->
|
||||||
val isSelected = selected is RoomGroupingMethod.BySpace && groupSummary.roomId == selected.space()?.roomId
|
val isSelected = selected is RoomGroupingMethod.BySpace && groupSummary.roomId == selected.space()?.roomId
|
||||||
// does it have children?
|
// does it have children?
|
||||||
|
@ -154,8 +154,12 @@ class SpaceSummaryController @Inject constructor(
|
||||||
id(groupSummary.roomId)
|
id(groupSummary.roomId)
|
||||||
hasChildren(hasChildren)
|
hasChildren(hasChildren)
|
||||||
expanded(expanded)
|
expanded(expanded)
|
||||||
|
// to debug order
|
||||||
|
// matrixItem(groupSummary.copy(displayName = "${groupSummary.displayName} / ${spaceOrderInfo?.get(groupSummary.roomId)}")
|
||||||
|
// .toMatrixItem())
|
||||||
matrixItem(groupSummary.toMatrixItem())
|
matrixItem(groupSummary.toMatrixItem())
|
||||||
selected(isSelected)
|
selected(isSelected)
|
||||||
|
canDrag(true)
|
||||||
onMore { host.callback?.onSpaceSettings(groupSummary) }
|
onMore { host.callback?.onSpaceSettings(groupSummary) }
|
||||||
listener { host.callback?.onSpaceSelected(groupSummary) }
|
listener { host.callback?.onSpaceSelected(groupSummary) }
|
||||||
toggleExpand { host.callback?.onToggleExpand(groupSummary) }
|
toggleExpand { host.callback?.onToggleExpand(groupSummary) }
|
||||||
|
|
|
@ -51,6 +51,7 @@ abstract class SpaceSummaryItem : VectorEpoxyModel<SpaceSummaryItem.Holder>() {
|
||||||
@EpoxyAttribute var countState: UnreadCounterBadgeView.State = UnreadCounterBadgeView.State(0, false)
|
@EpoxyAttribute var countState: UnreadCounterBadgeView.State = UnreadCounterBadgeView.State(0, false)
|
||||||
@EpoxyAttribute var description: String? = null
|
@EpoxyAttribute var description: String? = null
|
||||||
@EpoxyAttribute var showSeparator: Boolean = false
|
@EpoxyAttribute var showSeparator: Boolean = false
|
||||||
|
@EpoxyAttribute var canDrag: Boolean = true
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
|
|
|
@ -28,23 +28,30 @@ import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.AppStateHandler
|
import im.vector.app.AppStateHandler
|
||||||
import im.vector.app.RoomGroupingMethod
|
import im.vector.app.RoomGroupingMethod
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import im.vector.app.group
|
import im.vector.app.group
|
||||||
import im.vector.app.space
|
import im.vector.app.space
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.functions.BiFunction
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
import org.matrix.android.sdk.api.query.ActiveSpaceFilter
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.group.groupSummaryQueryParams
|
import org.matrix.android.sdk.api.session.group.groupSummaryQueryParams
|
||||||
import org.matrix.android.sdk.api.session.room.RoomSortOrder
|
import org.matrix.android.sdk.api.session.room.RoomSortOrder
|
||||||
|
import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent
|
||||||
|
import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||||
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
|
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
|
||||||
|
import org.matrix.android.sdk.api.session.space.SpaceOrderUtils
|
||||||
|
import org.matrix.android.sdk.api.session.space.model.SpaceOrderContent
|
||||||
|
import org.matrix.android.sdk.api.session.space.model.TopLevelSpaceComparator
|
||||||
import org.matrix.android.sdk.api.session.user.model.User
|
import org.matrix.android.sdk.api.session.user.model.User
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import org.matrix.android.sdk.rx.asObservable
|
import org.matrix.android.sdk.rx.asObservable
|
||||||
|
@ -143,37 +150,86 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
|
||||||
}.disposeOnClear()
|
}.disposeOnClear()
|
||||||
}
|
}
|
||||||
|
|
||||||
// private fun observeSelectionState() {
|
|
||||||
// selectSubscribe(SpaceListViewState::selectedSpace) { spaceSummary ->
|
|
||||||
// if (spaceSummary != null) {
|
|
||||||
// // We only want to open group if the updated selectedGroup is a different one.
|
|
||||||
// if (currentGroupId != spaceSummary.roomId) {
|
|
||||||
// currentGroupId = spaceSummary.roomId
|
|
||||||
// _viewEvents.post(SpaceListViewEvents.OpenSpace)
|
|
||||||
// }
|
|
||||||
// appStateHandler.setCurrentSpace(spaceSummary.roomId)
|
|
||||||
// } else {
|
|
||||||
// // If selected group is null we force to default. It can happens when leaving the selected group.
|
|
||||||
// setState {
|
|
||||||
// copy(selectedSpace = this.asyncSpaces()?.find { it.roomId == ALL_COMMUNITIES_GROUP_ID })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
override fun handle(action: SpaceListAction) {
|
override fun handle(action: SpaceListAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is SpaceListAction.SelectSpace -> handleSelectSpace(action)
|
is SpaceListAction.SelectSpace -> handleSelectSpace(action)
|
||||||
is SpaceListAction.LeaveSpace -> handleLeaveSpace(action)
|
is SpaceListAction.LeaveSpace -> handleLeaveSpace(action)
|
||||||
SpaceListAction.AddSpace -> handleAddSpace()
|
SpaceListAction.AddSpace -> handleAddSpace()
|
||||||
is SpaceListAction.ToggleExpand -> handleToggleExpand(action)
|
is SpaceListAction.ToggleExpand -> handleToggleExpand(action)
|
||||||
is SpaceListAction.OpenSpaceInvite -> handleSelectSpaceInvite(action)
|
is SpaceListAction.OpenSpaceInvite -> handleSelectSpaceInvite(action)
|
||||||
is SpaceListAction.SelectLegacyGroup -> handleSelectGroup(action)
|
is SpaceListAction.SelectLegacyGroup -> handleSelectGroup(action)
|
||||||
|
is SpaceListAction.MoveSpace -> handleMoveSpace(action)
|
||||||
|
is SpaceListAction.OnEndDragging -> handleEndDragging()
|
||||||
|
is SpaceListAction.OnStartDragging -> handleStartDragging()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
|
var preDragExpandedState: Map<String, Boolean>? = null
|
||||||
|
private fun handleStartDragging() = withState { state ->
|
||||||
|
preDragExpandedState = state.expandedStates.toMap()
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
expandedStates = expandedStates.map {
|
||||||
|
it.key to false
|
||||||
|
}.toMap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEndDragging() {
|
||||||
|
// restore expanded state
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
expandedStates = preDragExpandedState.orEmpty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMoveSpace(action: SpaceListAction.MoveSpace) = withState { state ->
|
||||||
|
state.rootSpacesOrdered ?: return@withState
|
||||||
|
val orderCommands = SpaceOrderUtils.orderCommandsForMove(
|
||||||
|
state.rootSpacesOrdered.map {
|
||||||
|
it.roomId to (state.spaceOrderLocalEchos?.get(it.roomId) ?: state.spaceOrderInfo?.get(it.roomId))
|
||||||
|
},
|
||||||
|
action.spaceId,
|
||||||
|
action.delta
|
||||||
|
)
|
||||||
|
|
||||||
|
// local echo
|
||||||
|
val updatedLocalEchos = state.spaceOrderLocalEchos.orEmpty().toMutableMap().apply {
|
||||||
|
orderCommands.forEach {
|
||||||
|
this[it.spaceId] = it.order
|
||||||
|
}
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
rootSpacesOrdered = state.rootSpacesOrdered.toMutableList().apply {
|
||||||
|
val index = indexOfFirst { it.roomId == action.spaceId }
|
||||||
|
val moved = removeAt(index)
|
||||||
|
add(index + action.delta, moved)
|
||||||
|
},
|
||||||
|
spaceOrderLocalEchos = updatedLocalEchos
|
||||||
|
)
|
||||||
|
}
|
||||||
|
session.coroutineScope.launch {
|
||||||
|
orderCommands.forEach {
|
||||||
|
session.getRoom(it.spaceId)?.updateAccountData(RoomAccountDataTypes.EVENT_TYPE_SPACE_ORDER,
|
||||||
|
SpaceOrderContent(order = it.order).toContent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore expanded state
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
expandedStates = preDragExpandedState.orEmpty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state ->
|
private fun handleSelectSpace(action: SpaceListAction.SelectSpace) = withState { state ->
|
||||||
val groupingMethod = state.selectedGroupingMethod
|
val groupingMethod = state.selectedGroupingMethod
|
||||||
if (groupingMethod is RoomGroupingMethod.ByLegacyGroup || groupingMethod.space()?.roomId != action.spaceSummary?.roomId) {
|
if (groupingMethod is RoomGroupingMethod.ByLegacyGroup || groupingMethod.space()?.roomId != action.spaceSummary?.roomId) {
|
||||||
|
@ -224,24 +280,43 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
|
||||||
excludeType = listOf(/**RoomType.MESSAGING,$*/
|
excludeType = listOf(/**RoomType.MESSAGING,$*/
|
||||||
null)
|
null)
|
||||||
}
|
}
|
||||||
Observable.combineLatest<User?, List<RoomSummary>, List<RoomSummary>>(
|
|
||||||
session
|
val rxSession = session.rx()
|
||||||
.rx()
|
|
||||||
|
Observable.combineLatest<User?, List<RoomSummary>, List<RoomAccountDataEvent>, List<RoomSummary>>(
|
||||||
|
rxSession
|
||||||
.liveUser(session.myUserId)
|
.liveUser(session.myUserId)
|
||||||
.map {
|
.map {
|
||||||
it.getOrNull()
|
it.getOrNull()
|
||||||
},
|
},
|
||||||
session
|
rxSession
|
||||||
.rx()
|
|
||||||
.liveSpaceSummaries(spaceSummaryQueryParams),
|
.liveSpaceSummaries(spaceSummaryQueryParams),
|
||||||
BiFunction { _, communityGroups ->
|
session.accountDataService().getLiveRoomAccountDataEvents(setOf(RoomAccountDataTypes.EVENT_TYPE_SPACE_ORDER)).asObservable(),
|
||||||
|
{ _, communityGroups, _ ->
|
||||||
communityGroups
|
communityGroups
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.execute { async ->
|
.execute { async ->
|
||||||
|
val rootSpaces = session.spaceService().getRootSpaceSummaries()
|
||||||
|
val orders = rootSpaces.map {
|
||||||
|
it.roomId to session.getRoom(it.roomId)
|
||||||
|
?.getAccountDataEvent(RoomAccountDataTypes.EVENT_TYPE_SPACE_ORDER)
|
||||||
|
?.content.toModel<SpaceOrderContent>()
|
||||||
|
?.safeOrder()
|
||||||
|
}.toMap()
|
||||||
copy(
|
copy(
|
||||||
asyncSpaces = async,
|
asyncSpaces = async,
|
||||||
rootSpaces = session.spaceService().getRootSpaceSummaries()
|
rootSpacesOrdered = rootSpaces.sortedWith(TopLevelSpaceComparator(orders)),
|
||||||
|
spaceOrderInfo = orders
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear local echos on update
|
||||||
|
session.accountDataService()
|
||||||
|
.getLiveRoomAccountDataEvents(setOf(RoomAccountDataTypes.EVENT_TYPE_SPACE_ORDER))
|
||||||
|
.asObservable().execute {
|
||||||
|
copy(
|
||||||
|
spaceOrderLocalEchos = emptyMap()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
<item>
|
<item>
|
||||||
<shape>
|
<shape>
|
||||||
<solid android:color="@android:color/transparent" />
|
<solid android:color="?android:colorBackground" />
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue