Skip to content

Commit f1467f7

Browse files
committed
feat(chat): advance read pointers when opening chats
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 3ecfa56 commit f1467f7

6 files changed

Lines changed: 103 additions & 4 deletions

File tree

api/src/main/java/com/getcode/model/Chat.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ data class Chat(
3939
fun resetUnreadCount() = copy(unreadCount = 0)
4040
fun toggleMute() = copy(isMuted = !isMuted)
4141

42+
val newestMessage: ChatMessage?
43+
get() = messages.maxByOrNull { it.dateMillis }
44+
4245
val lastMessageMillis: Long?
43-
get() = messages.maxOfOrNull { it.dateMillis }
46+
get() = newestMessage?.dateMillis
4447
}
4548

4649
sealed interface Pointer {

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.getcode.model.ChatMessage
1212
import com.getcode.model.Cursor
1313
import com.getcode.model.ID
1414
import com.getcode.network.client.Client
15+
import com.getcode.network.client.advancePointer
1516
import com.getcode.network.client.fetchChats
1617
import com.getcode.network.client.fetchMessagesFor
1718
import com.getcode.network.client.setMuted
@@ -92,6 +93,27 @@ class HistoryController @Inject constructor(
9293
}
9394
}
9495

96+
suspend fun advanceReadPointer(chatId: ID) {
97+
val owner = owner() ?: return
98+
99+
_chats.update {
100+
it?.toMutableList()?.apply chats@{
101+
indexOfFirst { chat -> chat.id == chatId }
102+
.takeIf { index -> index >= 0 }
103+
?.let { index ->
104+
val chat = this[index]
105+
val newestMessage = chat.newestMessage
106+
if (newestMessage != null) {
107+
client.advancePointer(owner, chatId, newestMessage.id)
108+
.onSuccess {
109+
this[index] = chat.resetUnreadCount()
110+
}
111+
}
112+
}
113+
}?.toList()
114+
}
115+
}
116+
95117
suspend fun setMuted(chatId: ID, muted: Boolean): Result<Boolean> {
96118
val owner = owner() ?: return Result.failure(Throwable("No owner detected"))
97119

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

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package com.getcode.network.api
22

33
import com.codeinc.gen.chat.v1.ChatGrpc
44
import com.codeinc.gen.chat.v1.ChatService
5+
import com.codeinc.gen.chat.v1.ChatService.AdvancePointerRequest
6+
import com.codeinc.gen.chat.v1.ChatService.AdvancePointerResponse
57
import com.codeinc.gen.chat.v1.ChatService.GetChatsRequest
68
import com.codeinc.gen.chat.v1.ChatService.GetMessagesRequest
9+
import com.codeinc.gen.chat.v1.ChatService.Pointer.Kind
710
import com.codeinc.gen.chat.v1.ChatService.SetMuteStateRequest
811
import com.codeinc.gen.chat.v1.ChatService.SetMuteStateResponse
912
import com.getcode.ed25519.Ed25519
@@ -65,6 +68,25 @@ class ChatApi @Inject constructor(
6568
.flowOn(Dispatchers.IO)
6669
}
6770

71+
fun advancePointer(owner: KeyPair, chatId: ID, to: ID): Flow<AdvancePointerResponse> {
72+
val request = AdvancePointerRequest.newBuilder()
73+
.setChatId(ChatService.ChatId.newBuilder()
74+
.setValue(chatId.toByteArray().toByteString())
75+
.build()
76+
).setPointer(ChatService.Pointer.newBuilder()
77+
.setKindValue(Kind.READ_VALUE)
78+
.setValue(ChatService.ChatMessageId.newBuilder()
79+
.setValue(to.toByteArray().toByteString())
80+
)
81+
).setOwner(owner.publicKeyBytes.toSolanaAccount())
82+
.setSignature(owner)
83+
.build()
84+
85+
return api::advancePointer
86+
.callAsCancellableFlow(request)
87+
.flowOn(Dispatchers.IO)
88+
}
89+
6890
fun setMuteState(owner: KeyPair, chatId: ID, muted: Boolean): Flow<SetMuteStateResponse> {
6991
val request = SetMuteStateRequest.newBuilder()
7092
.setChatId(ChatService.ChatId.newBuilder()
@@ -81,23 +103,31 @@ class ChatApi @Inject constructor(
81103
}
82104
}
83105

84-
fun GetChatsRequest.Builder.setSignature(owner: KeyPair): GetChatsRequest.Builder {
106+
private fun GetChatsRequest.Builder.setSignature(owner: KeyPair): GetChatsRequest.Builder {
107+
val bos = ByteArrayOutputStream()
108+
buildPartial().writeTo(bos)
109+
setSignature(Ed25519.sign(bos.toByteArray(), owner).toSignature())
110+
111+
return this
112+
}
113+
114+
private fun GetMessagesRequest.Builder.setSignature(owner: KeyPair): GetMessagesRequest.Builder {
85115
val bos = ByteArrayOutputStream()
86116
buildPartial().writeTo(bos)
87117
setSignature(Ed25519.sign(bos.toByteArray(), owner).toSignature())
88118

89119
return this
90120
}
91121

92-
fun GetMessagesRequest.Builder.setSignature(owner: KeyPair): GetMessagesRequest.Builder {
122+
private fun SetMuteStateRequest.Builder.setSignature(owner: KeyPair): SetMuteStateRequest.Builder {
93123
val bos = ByteArrayOutputStream()
94124
buildPartial().writeTo(bos)
95125
setSignature(Ed25519.sign(bos.toByteArray(), owner).toSignature())
96126

97127
return this
98128
}
99129

100-
fun SetMuteStateRequest.Builder.setSignature(owner: KeyPair): SetMuteStateRequest.Builder {
130+
private fun AdvancePointerRequest.Builder.setSignature(owner: KeyPair): AdvancePointerRequest.Builder {
101131
val bos = ByteArrayOutputStream()
102132
buildPartial().writeTo(bos)
103133
setSignature(Ed25519.sign(bos.toByteArray(), owner).toSignature())

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,12 @@ suspend fun Client.fetchMessagesFor(owner: KeyPair, chatId: ID, cursor: Cursor?
2929
}.onFailure {
3030
Timber.e(t = it, "Failed fetching messages.")
3131
}
32+
}
33+
34+
suspend fun Client.advancePointer(
35+
owner: KeyPair,
36+
chatId: ID,
37+
to: ID,
38+
): Result<Unit> {
39+
return chatService.advancePointer(owner, chatId, to)
3240
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,39 @@ class ChatService @Inject constructor(
130130
}
131131
}.first()
132132
}
133+
134+
suspend fun advancePointer(
135+
owner: KeyPair,
136+
chatId: ID,
137+
to: ID,
138+
): Result<Unit> {
139+
return networkOracle.managedRequest(api.advancePointer(owner, chatId, to))
140+
.map { response ->
141+
when (response.result) {
142+
ChatService.AdvancePointerResponse.Result.OK -> {
143+
Result.success(Unit)
144+
}
145+
ChatService.AdvancePointerResponse.Result.CHAT_NOT_FOUND -> {
146+
val error = Throwable("Error: chat not found $chatId")
147+
Timber.e(t = error)
148+
Result.failure(error)
149+
}
150+
ChatService.AdvancePointerResponse.Result.MESSAGE_NOT_FOUND -> {
151+
val error = Throwable("Error: message not found $to")
152+
Timber.e(t = error)
153+
Result.failure(error)
154+
}
155+
ChatService.AdvancePointerResponse.Result.UNRECOGNIZED -> {
156+
val error = Throwable("Error: Unrecognized request.")
157+
Timber.e(t = error)
158+
Result.failure(error)
159+
}
160+
else -> {
161+
val error = Throwable("Error: Unknown")
162+
Timber.e(t = error)
163+
Result.failure(error)
164+
}
165+
}
166+
}.first()
167+
}
133168
}

app/src/main/java/com/getcode/view/main/chat/ChatViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class ChatViewModel @Inject constructor(
7777
stateFlow
7878
.map { it.chatId }
7979
.filterNotNull()
80+
.onEach { historyController.advanceReadPointer(it) }
8081
.flatMapLatest { historyController.chats }
8182
.flowOn(Dispatchers.IO)
8283
.filterNotNull()

0 commit comments

Comments
 (0)