Room list : handle direct rooms

This commit is contained in:
ganfra 2018-10-30 18:22:29 +01:00
parent ec27951850
commit bc8055c3cd
20 changed files with 351 additions and 43 deletions

View File

@ -1,16 +1,30 @@
package im.vector.riotredesign.features.home.room.list

import android.support.annotation.DrawableRes
import android.support.v4.content.ContextCompat
import android.view.ViewGroup
import android.widget.TextView
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel

data class RoomCategoryItem(
val title: CharSequence,
@DrawableRes val expandDrawable: Int,
val isExpanded: Boolean,
val listener: (() -> Unit)? = null
) : KotlinModel(R.layout.item_room_category) {

override fun bind() {
private val titleView by bind<TextView>(R.id.roomCategoryTitleView)
private val rootView by bind<ViewGroup>(R.id.roomCategoryRootView)

private val tintColor by lazy {
ContextCompat.getColor(rootView.context, R.color.bluey_grey_two)
}

override fun bind() {
val expandedArrowDrawableRes = if (isExpanded) R.drawable.ic_expand_more_white else R.drawable.ic_expand_less_white
val expandedArrowDrawable = ContextCompat.getDrawable(rootView.context, expandedArrowDrawableRes)
expandedArrowDrawable?.setTint(tintColor)
titleView.setCompoundDrawablesWithIntrinsicBounds(expandedArrowDrawable, null, null, null)
titleView.text = title
rootView.setOnClickListener { listener?.invoke() }
}
}

View File

@ -10,15 +10,49 @@ class RoomSummaryController(private val context: Context,
private val callback: Callback? = null
) : Typed2EpoxyController<List<RoomSummary>, RoomSummary>() {


private var directRoomsExpanded = true
private var groupRoomsExpanded = true

override fun buildModels(summaries: List<RoomSummary>?, selected: RoomSummary?) {

val directRooms = summaries?.filter { it.isDirect } ?: emptyList()
val groupRooms = summaries?.filter { !it.isDirect } ?: emptyList()

RoomCategoryItem(
title = "DIRECT MESSAGES",
expandDrawable = R.drawable.ic_expand_more_white
isExpanded = directRoomsExpanded,
listener = {
directRoomsExpanded = !directRoomsExpanded
setData(summaries, selected)
}
)
.id("direct_messages")
.addTo(this)

summaries?.forEach {
if (directRoomsExpanded) {
buildRoomModels(directRooms, selected)
}

RoomCategoryItem(
title = "GROUPS",
isExpanded = groupRoomsExpanded,
listener = {
groupRoomsExpanded = !groupRoomsExpanded
setData(summaries, selected)
}
)
.id("group_messages")
.addTo(this)

if (groupRoomsExpanded) {
buildRoomModels(groupRooms, selected)
}

}

private fun buildRoomModels(summaries: List<RoomSummary>, selected: RoomSummary?) {
summaries.forEach {
val roomSummaryViewHelper = RoomSummaryViewHelper(it)
RoomSummaryItem(
title = it.displayName,

View File

@ -6,6 +6,7 @@
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:id="@+id/roomCategoryRootView"
android:gravity="center_vertical"
android:minHeight="24dp"
android:padding="16dp"

View File

@ -4,5 +4,6 @@ data class RoomSummary(
val roomId: String,
val displayName: String = "",
val topic: String = "",
val avatarUrl: String = ""
val avatarUrl: String = "",
val isDirect: Boolean
)

View File

@ -11,24 +11,12 @@ object RoomSummaryMapper {
roomSummaryEntity.roomId,
roomSummaryEntity.displayName ?: "",
roomSummaryEntity.topic ?: "",
roomSummaryEntity.avatarUrl ?: ""
)
}

internal fun map(roomSummary: RoomSummary): RoomSummaryEntity {
return RoomSummaryEntity(
roomSummary.roomId,
roomSummary.displayName,
roomSummary.topic,
roomSummary.avatarUrl
roomSummaryEntity.avatarUrl ?: "",
roomSummaryEntity.isDirect
)
}
}

fun RoomSummaryEntity.asDomain(): RoomSummary {
return RoomSummaryMapper.map(this)
}

fun RoomSummaryEntity.asEntity(): RoomSummary {
return RoomSummaryMapper.map(this)
}

View File

@ -4,7 +4,6 @@ import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey

// TODO to be completed
open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var displayName: String? = "",
var avatarUrl: String? = "",
@ -13,6 +12,7 @@ open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var heroes: RealmList<String> = RealmList(),
var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0,
var isDirect: Boolean = false,
var isLatestSelected: Boolean = false
) : RealmObject() {


View File

@ -1,12 +1,19 @@
package im.vector.matrix.android.internal.di

import com.squareup.moshi.Moshi
import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory
import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter
import im.vector.matrix.android.internal.session.sync.model.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirectMessages
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataFallback

object MoshiProvider {

private val moshi: Moshi = Moshi.Builder()
.add(UriMoshiAdapter())
.add(RuntimeJsonAdapterFactory.of(UserAccountData::class.java, "type", UserAccountDataFallback::class.java)
.registerSubtype(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES)
)
.build()

fun providesMoshi(): Moshi {

View File

@ -0,0 +1,177 @@
/*
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.network.parsing;

import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonDataException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import javax.annotation.CheckReturnValue;

/**
* A JsonAdapter factory for polymorphic types. This is useful when the type is not known before
* decoding the JSON. This factory's adapters expect JSON in the format of a JSON object with a
* key whose value is a label that determines the type to which to map the JSON object.
*/
public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
final Class<T> baseType;
final String labelKey;
final Class fallbackType;
final Map<String, Type> labelToType = new LinkedHashMap<>();

/**
* @param baseType The base type for which this factory will create adapters. Cannot be Object.
* @param labelKey The key in the JSON object whose value determines the type to which to map the
* JSON object.
*/
@CheckReturnValue
public static <T> RuntimeJsonAdapterFactory<T> of(Class<T> baseType, String labelKey, Class<? extends T> fallbackType) {
if (baseType == null) throw new NullPointerException("baseType == null");
if (labelKey == null) throw new NullPointerException("labelKey == null");
if (baseType == Object.class) {
throw new IllegalArgumentException(
"The base type must not be Object. Consider using a marker interface.");
}
return new RuntimeJsonAdapterFactory<>(baseType, labelKey, fallbackType);
}

RuntimeJsonAdapterFactory(Class<T> baseType, String labelKey, Class fallbackType) {
this.baseType = baseType;
this.labelKey = labelKey;
this.fallbackType = fallbackType;
}

/**
* Register the subtype that can be created based on the label. When an unknown type is found
* during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label
* is found during decoding a {@linkplain JsonDataException} will be thrown.
*/
public RuntimeJsonAdapterFactory<T> registerSubtype(Class<? extends T> subtype, String label) {
if (subtype == null) throw new NullPointerException("subtype == null");
if (label == null) throw new NullPointerException("label == null");
if (labelToType.containsKey(label) || labelToType.containsValue(subtype)) {
throw new IllegalArgumentException("Subtypes and labels must be unique.");
}
labelToType.put(label, subtype);
return this;
}

@Override
public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (Types.getRawType(type) != baseType || !annotations.isEmpty()) {
return null;
}
int size = labelToType.size();
Map<String, JsonAdapter<Object>> labelToAdapter = new LinkedHashMap<>(size);
Map<Type, String> typeToLabel = new LinkedHashMap<>(size);
for (Map.Entry<String, Type> entry : labelToType.entrySet()) {
String label = entry.getKey();
Type typeValue = entry.getValue();
typeToLabel.put(typeValue, label);
labelToAdapter.put(label, moshi.adapter(typeValue));
}

final JsonAdapter<Object> fallbackAdapter = moshi.adapter(fallbackType);
JsonAdapter<Object> objectJsonAdapter = moshi.adapter(Object.class);

return new RuntimeJsonAdapter(labelKey, labelToAdapter, typeToLabel,
objectJsonAdapter, fallbackAdapter).nullSafe();
}

static final class RuntimeJsonAdapter extends JsonAdapter<Object> {
final String labelKey;
final Map<String, JsonAdapter<Object>> labelToAdapter;
final Map<Type, String> typeToLabel;
final JsonAdapter<Object> objectJsonAdapter;
final JsonAdapter<Object> fallbackAdapter;

RuntimeJsonAdapter(String labelKey, Map<String, JsonAdapter<Object>> labelToAdapter,
Map<Type, String> typeToLabel, JsonAdapter<Object> objectJsonAdapter,
JsonAdapter<Object> fallbackAdapter) {
this.labelKey = labelKey;
this.labelToAdapter = labelToAdapter;
this.typeToLabel = typeToLabel;
this.objectJsonAdapter = objectJsonAdapter;
this.fallbackAdapter = fallbackAdapter;
}

@Override
public Object fromJson(JsonReader reader) throws IOException {
JsonReader.Token peekedToken = reader.peek();
if (peekedToken != JsonReader.Token.BEGIN_OBJECT) {
throw new JsonDataException("Expected BEGIN_OBJECT but was " + peekedToken
+ " at path " + reader.getPath());
}
Object jsonValue = reader.readJsonValue();
Map<String, Object> jsonObject = (Map<String, Object>) jsonValue;
Object label = jsonObject.get(labelKey);
if (label == null) {
throw new JsonDataException("Missing label for " + labelKey);
}
if (!(label instanceof String)) {
throw new JsonDataException("Label for '"
+ labelKey
+ "' must be a string but was "
+ label
+ ", a "
+ label.getClass());
}
JsonAdapter<Object> adapter = labelToAdapter.get(label);
if (adapter == null) {
return fallbackAdapter.fromJsonValue(jsonValue);
}
return adapter.fromJsonValue(jsonValue);
}

@Override
public void toJson(JsonWriter writer, Object value) throws IOException {
Class<?> type = value.getClass();
String label = typeToLabel.get(type);
if (label == null) {
throw new IllegalArgumentException("Expected one of "
+ typeToLabel.keySet()
+ " but found "
+ value
+ ", a "
+ value.getClass()
+ ". Register this subtype.");
}
JsonAdapter<Object> adapter = labelToAdapter.get(label);
Map<String, Object> jsonValue = (Map<String, Object>) adapter.toJsonValue(value);

Map<String, Object> valueWithLabel = new LinkedHashMap<>(1 + jsonValue.size());
valueWithLabel.put(labelKey, label);
valueWithLabel.putAll(jsonValue);
objectJsonAdapter.toJson(writer, valueWithLabel);
}

@Override
public String toString() {
return "RuntimeJsonAdapter(" + labelKey + ")";
}
}
}

View File

@ -15,6 +15,7 @@ import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersRequest
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
@ -36,7 +37,7 @@ data class DefaultRoom(
override val roomSummary: LiveData<RoomSummary> by lazy {
val liveData = monarchy
.findAllMappedWithChanges(
{ realm -> RoomSummaryEntity.where(realm, roomId) },
{ realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) },
{ from -> from.asDomain() })

Transformations.map(liveData) {

View File

@ -8,6 +8,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.lastSelected
import im.vector.matrix.android.internal.database.query.where

@ -38,7 +39,7 @@ class DefaultRoomService(private val monarchy: Monarchy) : RoomService {

override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges(
{ realm -> RoomSummaryEntity.where(realm) },
{ realm -> RoomSummaryEntity.where(realm).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) },
{ it.asDomain() }
)
}

View File

@ -15,6 +15,7 @@ import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
import io.realm.Realm
import io.realm.kotlin.createObject

@ -29,7 +30,21 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy()
}

fun handleRoomSync(handlingStrategy: HandlingStrategy) {
fun handle(roomsSyncResponse: RoomsSyncResponse) {
handleRoomSync(RoomSyncHandler.HandlingStrategy.JOINED(roomsSyncResponse.join))
handleRoomSync(RoomSyncHandler.HandlingStrategy.INVITED(roomsSyncResponse.invite))
handleRoomSync(RoomSyncHandler.HandlingStrategy.LEFT(roomsSyncResponse.leave))

monarchy.runTransactionSync { realm ->
roomsSyncResponse.join.forEach { (roomId, roomSync) ->
handleEphemeral(realm, roomId, roomSync.ephemeral)
}
}
}

// PRIVATE METHODS *****************************************************************************

private fun handleRoomSync(handlingStrategy: HandlingStrategy) {
monarchy.runTransactionSync { realm ->
val rooms = when (handlingStrategy) {
is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) }
@ -38,18 +53,8 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
}
realm.insertOrUpdate(rooms)
}

if (handlingStrategy is HandlingStrategy.JOINED) {
monarchy.runTransactionSync { realm ->
handlingStrategy.data.forEach { (roomId, roomSync) ->
handleEphemeral(realm, roomId, roomSync.ephemeral)
}
}
}
}

// PRIVATE METHODS *****************************************************************************

private fun handleJoinedRoom(realm: Realm,
roomId: String,
roomSync: RoomSync): RoomEntity {

View File

@ -30,7 +30,11 @@ class SyncModule : Module {
}

scope(DefaultSession.SCOPE) {
SyncResponseHandler(get())
UserAccountDataSyncHandler(get())
}

scope(DefaultSession.SCOPE) {
SyncResponseHandler(get(), get())
}

scope(DefaultSession.SCOPE) {

View File

@ -3,21 +3,20 @@ package im.vector.matrix.android.internal.session.sync
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import timber.log.Timber

internal class SyncResponseHandler(private val roomSyncHandler: RoomSyncHandler) {
internal class SyncResponseHandler(private val roomSyncHandler: RoomSyncHandler,
private val userAccountDataSyncHandler: UserAccountDataSyncHandler) {

fun handleResponse(syncResponse: SyncResponse?, fromToken: String?, isCatchingUp: Boolean) {
if (syncResponse == null) {
return
}
Timber.v("Handle sync response")

if (syncResponse.rooms != null) {
// joined rooms events
roomSyncHandler.handleRoomSync(RoomSyncHandler.HandlingStrategy.JOINED(syncResponse.rooms.join))
roomSyncHandler.handleRoomSync(RoomSyncHandler.HandlingStrategy.INVITED(syncResponse.rooms.invite))
roomSyncHandler.handleRoomSync(RoomSyncHandler.HandlingStrategy.LEFT(syncResponse.rooms.leave))
roomSyncHandler.handle(syncResponse.rooms)
}
if (syncResponse.accountData != null) {
userAccountDataSyncHandler.handle(syncResponse.accountData)
}

}



View File

@ -0,0 +1,37 @@
package im.vector.matrix.android.internal.session.sync

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataDirectMessages
import im.vector.matrix.android.internal.session.sync.model.UserAccountDataSync

class UserAccountDataSyncHandler(private val monarchy: Monarchy) {

fun handle(accountData: UserAccountDataSync) {
accountData.list.forEach {
when (it) {
is UserAccountDataDirectMessages -> handleDirectChatRooms(it)
else -> return@forEach
}
}
}

private fun handleDirectChatRooms(directMessages: UserAccountDataDirectMessages) {
val newDirectRoomIds = directMessages.content.values.flatten()
monarchy.runTransactionSync { realm ->

val oldDirectRooms = RoomSummaryEntity.where(realm).equalTo(RoomSummaryEntityFields.IS_DIRECT, true).findAll()
oldDirectRooms.forEach { it.isDirect = false }

newDirectRoomIds.forEach { roomId ->
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
if (roomSummaryEntity != null) {
roomSummaryEntity.isDirect = true
realm.insertOrUpdate(roomSummaryEntity)
}
}
}
}
}

View File

@ -10,7 +10,7 @@ data class SyncResponse(
/**
* The user private data.
*/
@Json(name = "account_data") val accountData: Map<String, Any>? = emptyMap(),
@Json(name = "account_data") val accountData: UserAccountDataSync? = null,

/**
* The opaque token for the end.

View File

@ -0,0 +1,11 @@
package im.vector.matrix.android.internal.session.sync.model

interface UserAccountData {

companion object {
const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list"
const val TYPE_DIRECT_MESSAGES = "m.direct"
const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls"
const val TYPE_WIDGETS = "m.widgets"
}
}

View File

@ -0,0 +1,10 @@
package im.vector.matrix.android.internal.session.sync.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class UserAccountDataDirectMessages(
@Json(name = "content") val content: Map<String, List<String>>
) : UserAccountData

View File

@ -0,0 +1,9 @@
package im.vector.matrix.android.internal.session.sync.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class UserAccountDataFallback(
@Json(name = "content") val content: Map<String, Any>
) : UserAccountData

View File

@ -0,0 +1,9 @@
package im.vector.matrix.android.internal.session.sync.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class UserAccountDataSync(
@Json(name = "events") val list: List<UserAccountData> = emptyList()
)