Skip to content

Commit e24aaf8

Browse files
committed
feat: update styling for messages within a ChatMessage; add mute support
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 405245d commit e24aaf8

11 files changed

Lines changed: 306 additions & 53 deletions

File tree

api/src/main/java/com/getcode/network/HistoryController.kt

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import com.getcode.ed25519.Ed25519.KeyPair
77
import com.getcode.manager.SessionManager
88
import com.getcode.model.Chat
99
import com.getcode.model.ChatMessage
10+
import com.getcode.model.Cursor
1011
import com.getcode.model.HistoricalTransaction
1112
import com.getcode.model.ID
1213
import com.getcode.network.client.Client
1314
import com.getcode.network.client.fetchChats
1415
import com.getcode.network.client.fetchMessagesFor
16+
import com.getcode.network.client.setMuted
1517
import com.getcode.network.repository.TransactionRepository
1618
import com.getcode.network.repository.encodeBase64
1719
import com.getcode.network.source.ChatMessagePagingSource
@@ -25,6 +27,7 @@ import kotlinx.coroutines.flow.catch
2527
import kotlinx.coroutines.flow.collect
2628
import kotlinx.coroutines.flow.filterNotNull
2729
import kotlinx.coroutines.flow.map
30+
import kotlinx.coroutines.flow.update
2831
import kotlinx.coroutines.reactive.asFlow
2932
import okhttp3.internal.toImmutableList
3033
import timber.log.Timber
@@ -46,7 +49,6 @@ class HistoryController @Inject constructor(
4649
.sortedByDescending { it.date }
4750
.toImmutableList()
4851

49-
5052
private val _chats = MutableStateFlow<List<Chat>?>(null)
5153
val chats: StateFlow<List<Chat>?>
5254
get() = _chats.asStateFlow()
@@ -93,7 +95,7 @@ class HistoryController @Inject constructor(
9395
Timber.d("chats fetched = ${containers.count()}")
9496
_chats.value = containers
9597

96-
val updatedWithMessages= mutableListOf<Chat>()
98+
val updatedWithMessages = mutableListOf<Chat>()
9799
containers.onEach { chat ->
98100
val result = fetchLatestMessageForChat(chat.id)
99101
result.onSuccess { message ->
@@ -108,6 +110,37 @@ class HistoryController @Inject constructor(
108110
_chats.value = updatedWithMessages.sortedByDescending { it.lastMessageMillis }
109111
}
110112

113+
suspend fun setMuted(chatId: ID, muted: Boolean): Result<Boolean> {
114+
val owner = owner() ?: return Result.failure(Throwable("No owner detected"))
115+
116+
_chats.update {
117+
it?.toMutableList()?.apply chats@{
118+
indexOfFirst { chat -> chat.id == chatId }
119+
.takeIf { index -> index >= 0 }
120+
?.let { index ->
121+
val chat = this[index]
122+
Timber.d("changing mute state for chat locally")
123+
this[index] = chat.copy(isMuted = muted)
124+
}
125+
}?.toList()
126+
}
127+
128+
return client.setMuted(owner, chatId, muted)
129+
}
130+
131+
suspend fun fetchMessagesForChat(
132+
id: List<Byte>,
133+
cursor: Cursor? = null,
134+
limit: Int? = null
135+
): Result<ChatMessage?> {
136+
val encodedId = id.toByteArray().encodeBase64()
137+
val owner = owner() ?: return Result.success(null)
138+
return client.fetchMessagesFor(owner, id, cursor, limit)
139+
.onFailure {
140+
Timber.e(t = it, "Failed to fetch messages for $encodedId.")
141+
}.map { it.getOrNull(0) }
142+
}
143+
111144
private suspend fun fetchLatestMessageForChat(id: List<Byte>): Result<ChatMessage?> {
112145
val encodedId = id.toByteArray().encodeBase64()
113146
Timber.d("fetching messages for $encodedId")

api/src/main/java/com/getcode/network/api/ChatApi.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import com.codeinc.gen.chat.v1.ChatGrpc
44
import com.codeinc.gen.chat.v1.ChatService
55
import com.codeinc.gen.chat.v1.ChatService.GetChatsRequest
66
import com.codeinc.gen.chat.v1.ChatService.GetMessagesRequest
7+
import com.codeinc.gen.chat.v1.ChatService.SetMuteStateRequest
8+
import com.codeinc.gen.chat.v1.ChatService.SetMuteStateResponse
79
import com.getcode.ed25519.Ed25519
810
import com.getcode.ed25519.Ed25519.KeyPair
911
import com.getcode.model.Cursor
@@ -62,6 +64,21 @@ class ChatApi @Inject constructor(
6264
.callAsCancellableFlow(request)
6365
.flowOn(Dispatchers.IO)
6466
}
67+
68+
fun setMuteState(owner: KeyPair, chatId: ID, muted: Boolean): Flow<SetMuteStateResponse> {
69+
val request = SetMuteStateRequest.newBuilder()
70+
.setChatId(ChatService.ChatId.newBuilder()
71+
.setValue(chatId.toByteArray().toByteString())
72+
.build()
73+
).setIsMuted(muted)
74+
.setOwner(owner.publicKeyBytes.toSolanaAccount())
75+
.setSignature(owner)
76+
.build()
77+
78+
return api::setMuteState
79+
.callAsCancellableFlow(request)
80+
.flowOn(Dispatchers.IO)
81+
}
6582
}
6683

6784
fun GetChatsRequest.Builder.setSignature(owner: KeyPair): GetChatsRequest.Builder {
@@ -77,5 +94,13 @@ fun GetMessagesRequest.Builder.setSignature(owner: KeyPair): GetMessagesRequest.
7794
buildPartial().writeTo(bos)
7895
setSignature(Ed25519.sign(bos.toByteArray(), owner).toSignature())
7996

97+
return this
98+
}
99+
100+
fun SetMuteStateRequest.Builder.setSignature(owner: KeyPair): SetMuteStateRequest.Builder {
101+
val bos = ByteArrayOutputStream()
102+
buildPartial().writeTo(bos)
103+
setSignature(Ed25519.sign(bos.toByteArray(), owner).toSignature())
104+
80105
return this
81106
}

api/src/main/java/com/getcode/network/client/Client_Chat.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ suspend fun Client.fetchChats(owner: KeyPair): Result<List<Chat>> {
1717
}
1818
}
1919

20+
suspend fun Client.setMuted(owner: KeyPair, chat: ID, muted: Boolean): Result<Boolean> {
21+
return chatService.setMuteState(owner, chat, muted)
22+
}
23+
2024
suspend fun Client.fetchMessagesFor(owner: KeyPair, chatId: ID, cursor: Cursor? = null, limit: Int? = null) : Result<List<ChatMessage>> {
2125
return chatService.fetchMessagesFor(owner, chatId, cursor, limit)
2226
.onSuccess {

api/src/main/java/com/getcode/network/service/ChatService.kt

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ class ChatService @Inject constructor(
4141
Timber.e(t = error)
4242
Result.failure(error)
4343
}
44+
4445
ChatService.GetChatsResponse.Result.UNRECOGNIZED -> {
4546
val error = Throwable("Error: Unrecognized request.")
4647
Timber.e(t = error)
4748
Result.failure(error)
4849
}
50+
4951
else -> {
5052
val error = Throwable("Error: Unknown")
5153
Timber.e(t = error)
@@ -54,12 +56,53 @@ class ChatService @Inject constructor(
5456
}
5557
}
5658
}
59+
5760
@Throws(NoSuchElementException::class)
5861
suspend fun fetchChats(owner: KeyPair): Result<List<Chat>> {
5962
return observeChats(owner).first()
6063
}
6164

62-
suspend fun fetchMessagesFor(owner: KeyPair, chatId: ID, cursor: Cursor? = null, limit: Int? = null): Result<List<ChatMessage>> {
65+
suspend fun setMuteState(owner: KeyPair, chatId: ID, muted: Boolean): Result<Boolean> {
66+
return networkOracle.managedRequest(api.setMuteState(owner, chatId, muted))
67+
.map { response ->
68+
when (response.result) {
69+
ChatService.SetMuteStateResponse.Result.OK -> {
70+
Result.success(muted)
71+
}
72+
73+
ChatService.SetMuteStateResponse.Result.CHAT_NOT_FOUND -> {
74+
val error = Throwable("Error: chat not found for $chatId")
75+
Timber.e(t = error)
76+
Result.failure(error)
77+
}
78+
79+
ChatService.SetMuteStateResponse.Result.CANT_MUTE -> {
80+
val error = Throwable("Error: Unable to change mute state for $chatId.")
81+
Timber.e(t = error)
82+
Result.failure(error)
83+
}
84+
85+
ChatService.SetMuteStateResponse.Result.UNRECOGNIZED -> {
86+
val error = Throwable("Error: Unrecognized request.")
87+
Timber.e(t = error)
88+
Result.failure(error)
89+
}
90+
91+
else -> {
92+
val error = Throwable("Error: Unknown")
93+
Timber.e(t = error)
94+
Result.failure(error)
95+
}
96+
}
97+
}.first()
98+
}
99+
100+
suspend fun fetchMessagesFor(
101+
owner: KeyPair,
102+
chatId: ID,
103+
cursor: Cursor? = null,
104+
limit: Int? = null
105+
): Result<List<ChatMessage>> {
63106
return networkOracle.managedRequest(api.fetchChatMessages(owner, chatId, cursor, limit))
64107
.map { response ->
65108
when (response.result) {
@@ -72,11 +115,13 @@ class ChatService @Inject constructor(
72115
Timber.e(t = error)
73116
Result.failure(error)
74117
}
118+
75119
ChatService.GetMessagesResponse.Result.UNRECOGNIZED -> {
76120
val error = Throwable("Error: Unrecognized request.")
77121
Timber.e(t = error)
78122
Result.failure(error)
79123
}
124+
80125
else -> {
81126
val error = Throwable("Error: Unknown")
82127
Timber.e(t = error)

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ dependencies {
140140
implementation(Libs.compose_ui_tools_preview)
141141
implementation(Libs.compose_foundation)
142142
implementation(Libs.compose_material)
143+
implementation(Libs.compose_materialIconsExtended)
143144
implementation(Libs.compose_activities)
144145
implementation(Libs.compose_view_models)
145146
implementation(Libs.compose_livedata)

app/src/main/java/com/getcode/view/components/chat/ChatNode.kt

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
package com.getcode.view.components.chat
22

33
import androidx.compose.animation.AnimatedVisibility
4-
import androidx.compose.foundation.background
54
import androidx.compose.foundation.clickable
65
import androidx.compose.foundation.layout.Arrangement
76
import androidx.compose.foundation.layout.Column
87
import androidx.compose.foundation.layout.Row
98
import androidx.compose.foundation.layout.fillMaxWidth
109
import androidx.compose.foundation.layout.padding
11-
import androidx.compose.foundation.shape.CircleShape
10+
import androidx.compose.material.Icon
1211
import androidx.compose.material.Text
12+
import androidx.compose.material.icons.Icons
13+
import androidx.compose.material.icons.automirrored.filled.VolumeOff
1314
import androidx.compose.runtime.Composable
1415
import androidx.compose.runtime.derivedStateOf
1516
import androidx.compose.runtime.getValue
1617
import androidx.compose.runtime.remember
1718
import androidx.compose.ui.Modifier
1819
import androidx.compose.ui.graphics.Color
1920
import androidx.compose.ui.platform.LocalContext
20-
import androidx.compose.ui.text.font.FontWeight
2121
import androidx.compose.ui.text.style.TextOverflow
2222
import com.getcode.BuildConfig
2323
import com.getcode.LocalCurrencyUtils
@@ -26,7 +26,7 @@ import com.getcode.model.Currency
2626
import com.getcode.model.GenericAmount
2727
import com.getcode.model.MessageContent
2828
import com.getcode.model.Title
29-
import com.getcode.model.Verb
29+
import com.getcode.theme.BrandLight
3030
import com.getcode.theme.CodeTheme
3131
import com.getcode.util.DateUtils
3232
import com.getcode.util.Kin
@@ -80,15 +80,22 @@ fun ChatNode(
8080
text = chat.messagePreview,
8181
style = CodeTheme.typography.body1,
8282
color = CodeTheme.colors.brandLight,
83-
minLines = 2,
8483
maxLines = 2,
8584
overflow = TextOverflow.Ellipsis
8685
)
87-
AnimatedVisibility(visible = hasUnreadMessages) {
88-
Badge(
89-
count = chat.unreadCount,
90-
color = ChatNodeDefaults.UnreadIndicator
86+
if (chat.isMuted) {
87+
Icon(
88+
Icons.AutoMirrored.Filled.VolumeOff,
89+
contentDescription = "chat is muted",
90+
tint = BrandLight
9191
)
92+
} else {
93+
AnimatedVisibility(visible = hasUnreadMessages) {
94+
Badge(
95+
count = chat.unreadCount,
96+
color = ChatNodeDefaults.UnreadIndicator
97+
)
98+
}
9299
}
93100
}
94101
}

0 commit comments

Comments
 (0)