fix(bots): sync cross-device group chats and roster from desktop projection (#1006)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
5bc6e5f855
commit
7e24f5eba3
4 files changed
+387
-104
No files matched your search
@@ -49,6 +49,21 @@ data class ProfileInfo(
|
||||
}
|
||||
}
|
||||
|
||||
fun groupChatSyncSnapshot(
|
||||
json: Json =
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
},
|
||||
): GroupChatSyncSnapshot? {
|
||||
val element = ui_meta?.get("hermes-bots-groups") ?: return null
|
||||
return try {
|
||||
json.decodeFromJsonElement<GroupChatSyncSnapshot>(element)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val effectiveTitle: String
|
||||
get() =
|
||||
botMeta()?.title?.takeIf { it.isNotBlank() }
|
||||
@@ -104,7 +119,15 @@ data class BotRosterMeta(
|
||||
val groups: List<String>? = null,
|
||||
val group: String? = null,
|
||||
val created: Double? = null,
|
||||
)
|
||||
) {
|
||||
val allGroups: List<String>
|
||||
get() {
|
||||
val list = mutableListOf<String>()
|
||||
groups?.forEach { if (it.isNotBlank()) list.add(it.trim()) }
|
||||
group?.takeIf { it.isNotBlank() }?.let { if (!list.contains(it.trim())) list.add(it.trim()) }
|
||||
return list.distinct()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class BotAvatarMeta(
|
||||
@@ -120,7 +143,62 @@ data class GroupChatSyncSnapshot(
|
||||
val updatedAt: Long? = null,
|
||||
val rooms: Map<String, GroupChatRoomMeta>? = null,
|
||||
val deleted: Map<String, Long>? = null,
|
||||
)
|
||||
) {
|
||||
fun toMap(): Map<String, Any?> {
|
||||
val roomsMap = mutableMapOf<String, Any?>()
|
||||
rooms?.forEach { (key, room) ->
|
||||
val roomMap = mutableMapOf<String, Any?>()
|
||||
room.name?.let { roomMap["name"] = it }
|
||||
room.roomId?.let { roomMap["roomId"] = it }
|
||||
room.picture?.let { roomMap["picture"] = it }
|
||||
room.image?.let { roomMap["image"] = it }
|
||||
room.updatedAt?.let { roomMap["updatedAt"] = it }
|
||||
room.createdAt?.let { roomMap["createdAt"] = it }
|
||||
room.revision?.let { roomMap["revision"] = it }
|
||||
room.syncRevision?.let { roomMap["syncRevision"] = it }
|
||||
room.tombstone?.let { roomMap["tombstone"] = it }
|
||||
room.members?.let { members ->
|
||||
roomMap["members"] =
|
||||
members.mapNotNull {
|
||||
when (it) {
|
||||
is JsonPrimitive -> it.content
|
||||
is JsonObject -> it["name"]?.jsonPrimitive?.content ?: it["handle"]?.jsonPrimitive?.content
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
room.log?.let { log ->
|
||||
roomMap["log"] =
|
||||
log.map { entry ->
|
||||
buildMap<String, Any?> {
|
||||
entry.id?.let { put("id", it) }
|
||||
entry.text?.let { put("text", it) }
|
||||
entry.at?.let { put("at", it) }
|
||||
entry.thread?.let { put("thread", it) }
|
||||
entry.from?.let { f ->
|
||||
put(
|
||||
"from",
|
||||
buildMap<String, Any?> {
|
||||
f.kind?.let { put("kind", it) }
|
||||
f.name?.let { put("name", it) }
|
||||
f.source?.let { put("source", it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
roomsMap[key] = roomMap
|
||||
}
|
||||
|
||||
return buildMap {
|
||||
put("version", version ?: 3)
|
||||
put("updatedAt", updatedAt ?: System.currentTimeMillis())
|
||||
put("rooms", roomsMap)
|
||||
deleted?.let { put("deleted", it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GroupChatRoomMeta(
|
||||
@@ -132,15 +210,17 @@ data class GroupChatRoomMeta(
|
||||
val image: String? = null,
|
||||
val log: List<GroupChatSyncLogEntry>? = null,
|
||||
val revision: Long? = null,
|
||||
val syncRevision: Long? = null,
|
||||
val updatedAt: Long? = null,
|
||||
val createdAt: Long? = null,
|
||||
val tombstone: Boolean? = null,
|
||||
) {
|
||||
val memberNames: List<String>
|
||||
get() =
|
||||
members?.mapNotNull { el ->
|
||||
when (el) {
|
||||
is JsonPrimitive -> el.content
|
||||
is JsonObject -> el["name"]?.jsonPrimitive?.content
|
||||
is JsonObject -> el["name"]?.jsonPrimitive?.content ?: el["handle"]?.jsonPrimitive?.content
|
||||
else -> null
|
||||
}
|
||||
}.orEmpty()
|
||||
|
||||
@@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.data.model.BotAvatarMeta
|
||||
import com.m57.hermescontrol.data.model.BotRosterMeta
|
||||
import com.m57.hermescontrol.data.model.CreateProfileRequest
|
||||
import com.m57.hermescontrol.data.model.GroupChatRoomMeta
|
||||
import com.m57.hermescontrol.data.model.GroupChatSyncSnapshot
|
||||
import com.m57.hermescontrol.data.model.ProfileInfo
|
||||
import com.m57.hermescontrol.data.model.ProfilesResponse
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
@@ -24,6 +26,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
|
||||
enum class BotsTab {
|
||||
@@ -34,6 +37,7 @@ enum class BotsTab {
|
||||
data class GroupInfo(
|
||||
val name: String,
|
||||
val members: List<ProfileInfo>,
|
||||
val lastActivity: Long = 0L,
|
||||
)
|
||||
|
||||
data class BotsUiState(
|
||||
@@ -52,17 +56,104 @@ data class BotsUiState(
|
||||
|
||||
val allGroups: List<GroupInfo>
|
||||
get() {
|
||||
val groupNames =
|
||||
profiles
|
||||
.flatMap { it.botMeta()?.groups.orEmpty() }
|
||||
.distinct()
|
||||
.sorted()
|
||||
return groupNames.map { gName ->
|
||||
val defaultProfile =
|
||||
profiles.find { it.is_default == true || it.name == "default" }
|
||||
?: profiles.firstOrNull()
|
||||
val syncSnapshot = defaultProfile?.groupChatSyncSnapshot()
|
||||
val deletedKeys = syncSnapshot?.deleted?.keys.orEmpty().map { it.lowercase() }
|
||||
|
||||
val groupNameMap = linkedMapOf<String, String>()
|
||||
|
||||
// 1. Collect from bot metadata (both `groups` array and `group` scalar)
|
||||
for (profile in profiles) {
|
||||
for (g in profile.botMeta()?.allGroups.orEmpty()) {
|
||||
val trimmed = g.trim()
|
||||
if (trimmed.isNotBlank()) {
|
||||
val lower = trimmed.lowercase()
|
||||
if (!groupNameMap.containsKey(lower)) {
|
||||
groupNameMap[lower] = trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Collect from cross-device snapshot rooms (Desktop parity)
|
||||
val snapshotRooms = syncSnapshot?.rooms.orEmpty()
|
||||
for ((key, room) in snapshotRooms) {
|
||||
if (room.tombstone == true) continue
|
||||
val roomKeyLower = key.lowercase()
|
||||
if (deletedKeys.contains(roomKeyLower) ||
|
||||
deletedKeys.contains("name:${room.name?.lowercase()}") ||
|
||||
deletedKeys.contains("id:${room.roomId?.lowercase()}")
|
||||
) {
|
||||
continue
|
||||
}
|
||||
val roomName =
|
||||
room.name?.trim()?.takeIf { it.isNotBlank() }
|
||||
?: key.removePrefix("name:").removePrefix("id:").trim()
|
||||
if (roomName.isNotBlank()) {
|
||||
val lower = roomName.lowercase()
|
||||
if (!groupNameMap.containsKey(lower)) {
|
||||
groupNameMap[lower] = roomName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupNameMap.values.map { gName ->
|
||||
val lower = gName.lowercase()
|
||||
val matchingRoom =
|
||||
snapshotRooms[gName]
|
||||
?: snapshotRooms["name:$gName"]
|
||||
?: snapshotRooms["id:$gName"]
|
||||
?: snapshotRooms.values.find { it.name.equals(gName, ignoreCase = true) }
|
||||
|
||||
val roomMemberNames = matchingRoom?.memberNames.orEmpty().map { it.lowercase() }
|
||||
|
||||
val matchedProfiles =
|
||||
profiles.filter { profile ->
|
||||
val botGroups = profile.botMeta()?.allGroups.orEmpty()
|
||||
botGroups.any { it.equals(gName, ignoreCase = true) } ||
|
||||
roomMemberNames.contains(profile.name.lowercase()) ||
|
||||
roomMemberNames.contains(profile.effectiveTitle.lowercase())
|
||||
}.toMutableList()
|
||||
|
||||
// If room listed members (e.g. remote bots or bots not in local profiles), ensure they are seated
|
||||
if (matchingRoom != null && roomMemberNames.isNotEmpty()) {
|
||||
for (mName in matchingRoom.memberNames) {
|
||||
val exists =
|
||||
matchedProfiles.any {
|
||||
it.name.equals(mName, ignoreCase = true) ||
|
||||
it.effectiveTitle.equals(mName, ignoreCase = true)
|
||||
}
|
||||
if (!exists) {
|
||||
val local = profiles.find { it.name.equals(mName, ignoreCase = true) }
|
||||
matchedProfiles.add(local ?: ProfileInfo(name = mName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var groupMembers: List<ProfileInfo> = matchedProfiles
|
||||
|
||||
// Fallback: If group name was composed from bot names (e.g. "default, scoutbot")
|
||||
if (groupMembers.isEmpty() && gName.contains(",")) {
|
||||
val targetNames = gName.split(",").map { it.trim().lowercase() }
|
||||
groupMembers = profiles.filter { targetNames.contains(it.name.lowercase()) }
|
||||
}
|
||||
|
||||
val lastAt =
|
||||
matchingRoom?.log?.lastOrNull()?.at
|
||||
?: matchingRoom?.updatedAt
|
||||
?: 0L
|
||||
|
||||
GroupInfo(
|
||||
name = gName,
|
||||
members = profiles.filter { it.botMeta()?.groups.orEmpty().contains(gName) },
|
||||
members = groupMembers,
|
||||
lastActivity = lastAt,
|
||||
)
|
||||
}
|
||||
}.sortedWith(
|
||||
compareByDescending<GroupInfo> { it.lastActivity }
|
||||
.thenBy { it.name },
|
||||
)
|
||||
}
|
||||
|
||||
val displayGroups: List<GroupInfo>
|
||||
@@ -321,13 +412,15 @@ class BotsViewModel(
|
||||
) {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
val currentProfiles = _uiState.value.profiles
|
||||
// 1. Update bot metadata on each member bot
|
||||
for (name in botNames) {
|
||||
val bot = currentProfiles.find { it.name == name } ?: continue
|
||||
val existingGroups = bot.botMeta()?.groups.orEmpty()
|
||||
if (!existingGroups.contains(groupName)) {
|
||||
val existingGroups = bot.botMeta()?.allGroups.orEmpty()
|
||||
if (!existingGroups.any { it.equals(groupName, ignoreCase = true) }) {
|
||||
val updatedMeta =
|
||||
(bot.botMeta() ?: BotRosterMeta()).copy(
|
||||
groups = existingGroups + groupName,
|
||||
group = existingGroups.firstOrNull() ?: groupName,
|
||||
)
|
||||
try {
|
||||
wsClientConfigureBot(name, updatedMeta).await()
|
||||
@@ -335,6 +428,47 @@ class BotsViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Also register room in hermes-bots-groups sync snapshot on default profile
|
||||
try {
|
||||
val defaultProfile =
|
||||
currentProfiles.find { it.is_default == true || it.name == "default" }
|
||||
?: currentProfiles.firstOrNull()
|
||||
if (defaultProfile != null) {
|
||||
val existingSnapshot =
|
||||
defaultProfile.groupChatSyncSnapshot()
|
||||
?: GroupChatSyncSnapshot(version = 3, rooms = emptyMap())
|
||||
val roomKey = "name:$groupName"
|
||||
val newRoom =
|
||||
GroupChatRoomMeta(
|
||||
name = groupName,
|
||||
members = botNames.map { JsonPrimitive(it) },
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
createdAt = System.currentTimeMillis(),
|
||||
)
|
||||
val updatedRooms = existingSnapshot.rooms.orEmpty() + (roomKey to newRoom)
|
||||
val updatedDeleted =
|
||||
existingSnapshot.deleted.orEmpty().filterKeys {
|
||||
!it.equals(roomKey, ignoreCase = true) && !it.equals(groupName, ignoreCase = true)
|
||||
}
|
||||
val newSnapshot =
|
||||
existingSnapshot.copy(
|
||||
version = 3,
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
rooms = updatedRooms,
|
||||
deleted = updatedDeleted,
|
||||
)
|
||||
HermesWsClient.request(
|
||||
WsMethods.PROFILES_CONFIGURE,
|
||||
mapOf(
|
||||
"name" to defaultProfile.name,
|
||||
"ui_meta" to mapOf("hermes-bots-groups" to newSnapshot.toMap()),
|
||||
),
|
||||
).await()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
loadBots()
|
||||
onSuccess()
|
||||
}
|
||||
@@ -346,12 +480,15 @@ class BotsViewModel(
|
||||
) {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
val currentProfiles = _uiState.value.profiles
|
||||
// 1. Remove from all member bots' metadata
|
||||
for (bot in currentProfiles) {
|
||||
val existingGroups = bot.botMeta()?.groups.orEmpty()
|
||||
if (existingGroups.contains(groupName)) {
|
||||
val existingGroups = bot.botMeta()?.allGroups.orEmpty()
|
||||
if (existingGroups.any { it.equals(groupName, ignoreCase = true) }) {
|
||||
val filtered = existingGroups.filterNot { it.equals(groupName, ignoreCase = true) }
|
||||
val updatedMeta =
|
||||
(bot.botMeta() ?: BotRosterMeta()).copy(
|
||||
groups = existingGroups - groupName,
|
||||
groups = filtered,
|
||||
group = filtered.firstOrNull(),
|
||||
)
|
||||
try {
|
||||
wsClientConfigureBot(bot.name, updatedMeta).await()
|
||||
@@ -359,6 +496,44 @@ class BotsViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remove room from hermes-bots-groups snapshot and register tombstone in deleted map
|
||||
try {
|
||||
val defaultProfile =
|
||||
currentProfiles.find { it.is_default == true || it.name == "default" }
|
||||
?: currentProfiles.firstOrNull()
|
||||
if (defaultProfile != null) {
|
||||
val existingSnapshot = defaultProfile.groupChatSyncSnapshot()
|
||||
if (existingSnapshot != null) {
|
||||
val updatedRooms =
|
||||
existingSnapshot.rooms.orEmpty().filterKeys { key ->
|
||||
val room = existingSnapshot.rooms?.get(key)
|
||||
!key.equals(groupName, ignoreCase = true) &&
|
||||
!key.equals("name:$groupName", ignoreCase = true) &&
|
||||
!key.equals("id:$groupName", ignoreCase = true) &&
|
||||
!(room?.name.equals(groupName, ignoreCase = true))
|
||||
}
|
||||
val deletedMap =
|
||||
existingSnapshot.deleted.orEmpty() + ("name:$groupName" to System.currentTimeMillis())
|
||||
val newSnapshot =
|
||||
existingSnapshot.copy(
|
||||
version = 3,
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
rooms = updatedRooms,
|
||||
deleted = deletedMap,
|
||||
)
|
||||
HermesWsClient.request(
|
||||
WsMethods.PROFILES_CONFIGURE,
|
||||
mapOf(
|
||||
"name" to defaultProfile.name,
|
||||
"ui_meta" to mapOf("hermes-bots-groups" to newSnapshot.toMap()),
|
||||
),
|
||||
).await()
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
loadBots()
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
@@ -28,10 +28,8 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import java.util.UUID
|
||||
|
||||
data class GroupChatUiState(
|
||||
@@ -170,11 +168,44 @@ class GroupChatViewModel(
|
||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||
val allProfiles = fetchProfiles()
|
||||
if (allProfiles.isNotEmpty()) {
|
||||
var groupMembers =
|
||||
// Hydrate previous group chat messages from cross-device sync snapshot
|
||||
val defaultProfile =
|
||||
allProfiles.find { it.is_default == true || it.name == "default" }
|
||||
?: allProfiles.firstOrNull()
|
||||
val syncSnapshot = defaultProfile?.groupChatSyncSnapshot()
|
||||
|
||||
val matchingRoom =
|
||||
syncSnapshot?.rooms?.get(groupName)
|
||||
?: syncSnapshot?.rooms?.get("name:$groupName")
|
||||
?: syncSnapshot?.rooms?.get("id:$groupName")
|
||||
?: syncSnapshot?.rooms?.values?.find { it.name.equals(groupName, ignoreCase = true) }
|
||||
|
||||
val roomMemberNames = matchingRoom?.memberNames.orEmpty().map { it.lowercase() }
|
||||
|
||||
val matchedProfiles =
|
||||
allProfiles.filter {
|
||||
val botGroups = it.botMeta()?.groups.orEmpty()
|
||||
botGroups.contains(groupName) || botGroups.any { g -> g.equals(groupName, ignoreCase = true) }
|
||||
val botGroups = it.botMeta()?.allGroups.orEmpty()
|
||||
botGroups.any { g -> g.equals(groupName, ignoreCase = true) } ||
|
||||
roomMemberNames.contains(it.name.lowercase()) ||
|
||||
roomMemberNames.contains(it.effectiveTitle.lowercase())
|
||||
}.toMutableList()
|
||||
|
||||
// If room listed members (e.g. remote bots or bots not in local profiles), ensure they are seated
|
||||
if (matchingRoom != null && roomMemberNames.isNotEmpty()) {
|
||||
for (mName in matchingRoom.memberNames) {
|
||||
val exists =
|
||||
matchedProfiles.any {
|
||||
it.name.equals(mName, ignoreCase = true) ||
|
||||
it.effectiveTitle.equals(mName, ignoreCase = true)
|
||||
}
|
||||
if (!exists) {
|
||||
val local = allProfiles.find { it.name.equals(mName, ignoreCase = true) }
|
||||
matchedProfiles.add(local ?: ProfileInfo(name = mName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var groupMembers: List<ProfileInfo> = matchedProfiles
|
||||
|
||||
// Fallback: If group name was composed from bot names (e.g. "default, scoutbot")
|
||||
if (groupMembers.isEmpty() && groupName.contains(",")) {
|
||||
@@ -192,34 +223,19 @@ class GroupChatViewModel(
|
||||
"Resolved ${groupMembers.size} members for group '$groupName': ${groupMembers.map { it.name }}",
|
||||
)
|
||||
|
||||
// Hydrate previous group chat messages from cross-device sync snapshot
|
||||
val defaultProfile = allProfiles.find { it.is_default == true || it.name == "default" }
|
||||
val syncSnapshotElement = defaultProfile?.ui_meta?.get("hermes-bots-groups")
|
||||
val syncSnapshot =
|
||||
syncSnapshotElement?.let { el ->
|
||||
try {
|
||||
json.decodeFromJsonElement<GroupChatSyncSnapshot>(el)
|
||||
} catch (e: Exception) {
|
||||
Log.w("GroupChatViewModel", "Failed to decode sync snapshot: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
var initialMessages = _uiState.value.messages
|
||||
if (initialMessages.isEmpty() && syncSnapshot?.rooms != null) {
|
||||
val matchingRoom =
|
||||
syncSnapshot.rooms[groupName]
|
||||
?: syncSnapshot.rooms["name:$groupName"]
|
||||
?: syncSnapshot.rooms["id:$groupName"]
|
||||
?: syncSnapshot.rooms.values.find { it.name.equals(groupName, ignoreCase = true) }
|
||||
|
||||
val syncedLog = matchingRoom?.log.orEmpty()
|
||||
if (syncedLog.isNotEmpty()) {
|
||||
initialMessages =
|
||||
syncedLog.map { entry ->
|
||||
val isUser = entry.from?.kind != "member"
|
||||
val senderName = entry.from?.name.orEmpty().ifBlank { if (isUser) "user" else "bot" }
|
||||
val memberProfile = allProfiles.find { it.name.equals(senderName, ignoreCase = true) }
|
||||
val memberProfile =
|
||||
allProfiles.find {
|
||||
it.name.equals(senderName, ignoreCase = true) ||
|
||||
it.effectiveTitle.equals(senderName, ignoreCase = true)
|
||||
}
|
||||
GroupChatMessage(
|
||||
id = entry.id ?: UUID.randomUUID().toString(),
|
||||
senderName = memberProfile?.name ?: senderName,
|
||||
@@ -446,15 +462,9 @@ class GroupChatViewModel(
|
||||
val allProfiles = fetchProfiles()
|
||||
val defaultProfile =
|
||||
allProfiles.find { it.is_default == true || it.name == "default" } ?: return@launch
|
||||
val existingElement = defaultProfile.ui_meta?.get("hermes-bots-groups")
|
||||
val existingSnapshot =
|
||||
existingElement?.let {
|
||||
try {
|
||||
json.decodeFromJsonElement<GroupChatSyncSnapshot>(it)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
} ?: GroupChatSyncSnapshot(version = 3, rooms = emptyMap())
|
||||
defaultProfile.groupChatSyncSnapshot()
|
||||
?: GroupChatSyncSnapshot(version = 3, rooms = emptyMap())
|
||||
|
||||
val logEntries =
|
||||
currentMessages.takeLast(32).map { msg ->
|
||||
@@ -472,8 +482,13 @@ class GroupChatViewModel(
|
||||
}
|
||||
|
||||
val roomKey = "name:$groupName"
|
||||
val existingRoom =
|
||||
existingSnapshot.rooms?.get(roomKey)
|
||||
?: existingSnapshot.rooms?.get(groupName)
|
||||
?: existingSnapshot.rooms?.values?.find { it.name.equals(groupName, ignoreCase = true) }
|
||||
|
||||
val updatedRoom =
|
||||
GroupChatRoomMeta(
|
||||
(existingRoom ?: GroupChatRoomMeta(name = groupName, createdAt = System.currentTimeMillis())).copy(
|
||||
name = groupName,
|
||||
members = _uiState.value.members.map { JsonPrimitive(it.name) },
|
||||
log = logEntries,
|
||||
@@ -488,7 +503,7 @@ class GroupChatViewModel(
|
||||
rooms = updatedRooms,
|
||||
)
|
||||
|
||||
val uiMetaPayload = mapOf("hermes-bots-groups" to snapshotToMap(newSnapshot))
|
||||
val uiMetaPayload = mapOf("hermes-bots-groups" to newSnapshot.toMap())
|
||||
|
||||
HermesWsClient.request(
|
||||
WsMethods.PROFILES_CONFIGURE,
|
||||
@@ -503,59 +518,6 @@ class GroupChatViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun snapshotToMap(snapshot: GroupChatSyncSnapshot): Map<String, Any?> {
|
||||
val roomsMap = mutableMapOf<String, Any?>()
|
||||
snapshot.rooms?.forEach { (key, room) ->
|
||||
val roomMap = mutableMapOf<String, Any?>()
|
||||
room.name?.let { roomMap["name"] = it }
|
||||
room.roomId?.let { roomMap["roomId"] = it }
|
||||
room.picture?.let { roomMap["picture"] = it }
|
||||
room.image?.let { roomMap["image"] = it }
|
||||
room.updatedAt?.let { roomMap["updatedAt"] = it }
|
||||
room.createdAt?.let { roomMap["createdAt"] = it }
|
||||
room.revision?.let { roomMap["revision"] = it }
|
||||
room.members?.let { members ->
|
||||
roomMap["members"] =
|
||||
members.mapNotNull {
|
||||
when (it) {
|
||||
is JsonPrimitive -> it.content
|
||||
is JsonObject -> it["name"]?.jsonPrimitive?.content
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
room.log?.let { log ->
|
||||
roomMap["log"] =
|
||||
log.map { entry ->
|
||||
buildMap<String, Any?> {
|
||||
entry.id?.let { put("id", it) }
|
||||
entry.text?.let { put("text", it) }
|
||||
entry.at?.let { put("at", it) }
|
||||
entry.thread?.let { put("thread", it) }
|
||||
entry.from?.let { f ->
|
||||
put(
|
||||
"from",
|
||||
buildMap<String, Any?> {
|
||||
f.kind?.let { put("kind", it) }
|
||||
f.name?.let { put("name", it) }
|
||||
f.source?.let { put("source", it) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
roomsMap[key] = roomMap
|
||||
}
|
||||
|
||||
return buildMap {
|
||||
put("version", snapshot.version ?: 3)
|
||||
put("updatedAt", snapshot.updatedAt ?: System.currentTimeMillis())
|
||||
put("rooms", roomsMap)
|
||||
snapshot.deleted?.let { put("deleted", it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPass(text: String): Boolean {
|
||||
val clean = text.trim().lowercase()
|
||||
return clean == "(pass)" || clean == "pass"
|
||||
|
||||
Reference in new issue
Block a user