|
|
|
|
@ -21,6 +21,8 @@ import kotlinx.coroutines.launch
|
|
|
|
|
import kotlinx.coroutines.withContext |
|
|
|
|
import java.io.File |
|
|
|
|
import java.io.FileOutputStream |
|
|
|
|
import org.json.JSONObject |
|
|
|
|
import org.json.JSONArray |
|
|
|
|
|
|
|
|
|
private val IMAGE_EXTENSIONS = setOf("jpg", "jpeg", "png", "gif", "bmp", "webp") |
|
|
|
|
|
|
|
|
|
@ -111,6 +113,12 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
|
|
|
|
|
private val _messages = MutableStateFlow<List<Message>>(emptyList()) |
|
|
|
|
val messages: StateFlow<List<Message>> = _messages |
|
|
|
|
private val feedbackCache = mutableMapOf<String, FeedbackState>() |
|
|
|
|
private val _reactionCatalog = MutableStateFlow<List<ReactionState>>(emptyList()) |
|
|
|
|
val reactionCatalog: StateFlow<List<ReactionState>> = _reactionCatalog |
|
|
|
|
private val _interactionFailed = MutableStateFlow(false) |
|
|
|
|
val interactionFailed: StateFlow<Boolean> = _interactionFailed |
|
|
|
|
private var nextInteractionId = 0L |
|
|
|
|
|
|
|
|
|
private val _currentChannel = MutableStateFlow<Channel?>(null) |
|
|
|
|
val currentChannel: StateFlow<Channel?> = _currentChannel |
|
|
|
|
@ -284,6 +292,35 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
_dmConversations.value = _dmConversations.value.map { it.copy(online = false) } |
|
|
|
|
} else if (type in listOf(4, 5, 12, 16, 17, 24, 25, 31, 33)) refreshDmCallRoute() |
|
|
|
|
when (type) { |
|
|
|
|
53 -> { /* FEEDBACK_CHANGED: channel, message_id, kind. */ |
|
|
|
|
if (data == null || data.size < 34) return |
|
|
|
|
val size = data[0].toInt() and 255 |
|
|
|
|
if (data.size != size + 34) { LogManager.addLog("ERROR", "POLL", "invalid change event"); return } |
|
|
|
|
val channel = String(data, 1, size, Charsets.UTF_8) |
|
|
|
|
if (_currentChannel.value?.id != channel) return |
|
|
|
|
val id = data.copyOfRange(size + 1, size + 33).joinToString("") { "%02x".format(it.toInt() and 255) } |
|
|
|
|
sendInteraction(JSONObject().put("cmd", "feedback").put("ch", channel).put("message_id", id), false) |
|
|
|
|
} |
|
|
|
|
54 -> { /* FEEDBACK_RESULT: request_id:8LE, success, UTF-8 JSON. */ |
|
|
|
|
if (data == null || data.size < 9) return |
|
|
|
|
try { |
|
|
|
|
val id = java.nio.ByteBuffer.wrap(data, 0, 8).order(java.nio.ByteOrder.LITTLE_ENDIAN).long |
|
|
|
|
val result = JSONObject(String(data, 9, data.size - 9, Charsets.UTF_8)) |
|
|
|
|
if (data[8].toInt() == 0) { |
|
|
|
|
LogManager.addLog("ERROR", "POLL", "command failed: ${result.optString("error")}") |
|
|
|
|
if (id != 0L) _interactionFailed.value = true |
|
|
|
|
} else if (result.has("catalog")) { |
|
|
|
|
val values = result.getJSONArray("catalog") |
|
|
|
|
_reactionCatalog.value = (0 until values.length()).map { index -> values.getJSONObject(index).let { |
|
|
|
|
ReactionState(it.getInt("id"), it.getString("emoji")) |
|
|
|
|
} } |
|
|
|
|
} else if (result.optString("ch") == _currentChannel.value?.id) { |
|
|
|
|
val state = parseFeedback(result) |
|
|
|
|
feedbackCache[state.messageId] = state |
|
|
|
|
_messages.update { rows -> rows.map { if (it.messageId == state.messageId) it.copy(feedback = state) else it } } |
|
|
|
|
} |
|
|
|
|
} catch (e: Exception) { LogManager.addLog("ERROR", "POLL", "invalid result: ${e.message}"); _interactionFailed.value = true } |
|
|
|
|
} |
|
|
|
|
9 -> { /* DB_READY */ |
|
|
|
|
if (dbReady) return |
|
|
|
|
repo = ChatRepository() |
|
|
|
|
@ -937,13 +974,45 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
fun refreshMessages(chId: String) { |
|
|
|
|
val r = repo ?: return |
|
|
|
|
viewModelScope.launch { |
|
|
|
|
_messages.value = withContext(Dispatchers.IO) { r.getMessages(chId) } |
|
|
|
|
val rows = withContext(Dispatchers.IO) { r.getMessages(chId) } |
|
|
|
|
if (_currentChannel.value?.id != chId) return@launch |
|
|
|
|
_messages.value = rows.map { it.copy(feedback = feedbackCache[it.messageId]) } |
|
|
|
|
if (_reactionCatalog.value.isEmpty()) sendInteraction(JSONObject().put("cmd", "reaction_catalog"), false) |
|
|
|
|
for (row in rows) if (row.messageId.length == 64) |
|
|
|
|
sendInteraction(JSONObject().put("cmd", "feedback").put("ch", chId).put("message_id", row.messageId), false) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun sendInteraction(command: JSONObject, userAction: Boolean = true) { |
|
|
|
|
if (!command.has("ch")) command.put("ch", _currentChannel.value?.id ?: return) |
|
|
|
|
if (!NativeLib.feedbackCommand(command.toString(), if (userAction) ++nextInteractionId else 0)) { |
|
|
|
|
LogManager.addLog("ERROR", "POLL", "cannot queue command ${command.optString("cmd")}") |
|
|
|
|
if (userAction) _interactionFailed.value = true |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun createPoll(question: String, options: List<String>, multiple: Boolean, custom: Boolean) { |
|
|
|
|
sendInteraction(JSONObject().put("cmd", "poll_create").put("question", question) |
|
|
|
|
.put("options", JSONArray(options)).put("mode", if (multiple) "multiple" else "single").put("allow_custom_answer", custom)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun vote(message: Message, choices: List<Int>, custom: String) { |
|
|
|
|
sendInteraction(JSONObject().put("cmd", "vote").put("message_id", message.messageId) |
|
|
|
|
.put("choices", JSONArray(choices)).put("custom", custom)) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun react(message: Message, emoji: Int?) { |
|
|
|
|
val command = JSONObject().put("cmd", "react").put("message_id", message.messageId) |
|
|
|
|
if (emoji == null) command.put("clear", true) else command.put("emoji", emoji) |
|
|
|
|
sendInteraction(command) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun dismissInteractionError() { _interactionFailed.value = false } |
|
|
|
|
|
|
|
|
|
fun selectChannel(channel: Channel) { |
|
|
|
|
val r = repo ?: return |
|
|
|
|
if (_currentChannel.value?.id != channel.id) { |
|
|
|
|
feedbackCache.clear() |
|
|
|
|
memberDetailJob?.cancel(); memberDetailTarget = null; _selectedMember.value = null |
|
|
|
|
} |
|
|
|
|
_currentChannel.value = channel |
|
|
|
|
|