Skip to content

Commit eb1c650

Browse files
authored
chore: cache inbox message opened (#656)
1 parent 70d9831 commit eb1c650

File tree

4 files changed

+117
-39
lines changed

4 files changed

+117
-39
lines changed

messaginginapp/api/messaginginapp.api

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,6 @@ public final class io/customer/messaginginapp/gist/data/NetworkUtilities {
5858
public final class io/customer/messaginginapp/gist/data/NetworkUtilities$Companion {
5959
}
6060

61-
public abstract interface class io/customer/messaginginapp/gist/data/listeners/GistQueue {
62-
public abstract fun fetchUserMessages ()V
63-
public abstract fun logDeleted (Lio/customer/messaginginapp/gist/data/model/InboxMessage;)V
64-
public abstract fun logOpenedStatus (Lio/customer/messaginginapp/gist/data/model/InboxMessage;Z)V
65-
public abstract fun logView (Lio/customer/messaginginapp/gist/data/model/Message;)V
66-
}
67-
68-
public final class io/customer/messaginginapp/gist/data/listeners/Queue : io/customer/messaginginapp/gist/data/listeners/GistQueue {
69-
public fun <init> ()V
70-
public fun fetchUserMessages ()V
71-
public fun logDeleted (Lio/customer/messaginginapp/gist/data/model/InboxMessage;)V
72-
public fun logOpenedStatus (Lio/customer/messaginginapp/gist/data/model/InboxMessage;Z)V
73-
public fun logView (Lio/customer/messaginginapp/gist/data/model/Message;)V
74-
}
75-
7661
public final class io/customer/messaginginapp/gist/data/model/BroadcastFrequency {
7762
public fun <init> (IIZ)V
7863
public synthetic fun <init> (IIZILkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -244,24 +229,6 @@ public final class io/customer/messaginginapp/inbox/MessageInbox {
244229
public static synthetic fun trackMessageClicked$default (Lio/customer/messaginginapp/inbox/MessageInbox;Lio/customer/messaginginapp/gist/data/model/InboxMessage;Ljava/lang/String;ILjava/lang/Object;)V
245230
}
246231

247-
public abstract interface class io/customer/messaginginapp/store/InAppPreferenceStore {
248-
public abstract fun clearAll ()V
249-
public abstract fun clearAllAnonymousData ()V
250-
public abstract fun clearAnonymousTracking (Ljava/lang/String;)V
251-
public abstract fun getAnonymousMessages ()Ljava/lang/String;
252-
public abstract fun getAnonymousNextShowTime (Ljava/lang/String;)J
253-
public abstract fun getAnonymousTimesShown (Ljava/lang/String;)I
254-
public abstract fun getNetworkResponse (Ljava/lang/String;)Ljava/lang/String;
255-
public abstract fun incrementAnonymousTimesShown (Ljava/lang/String;)V
256-
public abstract fun isAnonymousDismissed (Ljava/lang/String;)Z
257-
public abstract fun isAnonymousInDelayPeriod (Ljava/lang/String;)Z
258-
public abstract fun isAnonymousMessagesExpired ()Z
259-
public abstract fun saveAnonymousMessages (Ljava/lang/String;J)V
260-
public abstract fun saveNetworkResponse (Ljava/lang/String;Ljava/lang/String;)V
261-
public abstract fun setAnonymousDismissed (Ljava/lang/String;Z)V
262-
public abstract fun setAnonymousNextShowTime (Ljava/lang/String;J)V
263-
}
264-
265232
public abstract interface class io/customer/messaginginapp/type/InAppEventListener {
266233
public abstract fun errorWithMessage (Lio/customer/messaginginapp/type/InAppMessage;)V
267234
public abstract fun messageActionTaken (Lio/customer/messaginginapp/type/InAppMessage;Ljava/lang/String;Ljava/lang/String;)V

messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/listeners/Queue.kt

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ internal interface GistQueueService {
4848
suspend fun logInboxMessageOpened(@Path("queueId") queueId: String, @Query("sessionId") sessionId: String, @Body body: Map<String, Boolean>)
4949
}
5050

51-
interface GistQueue {
51+
internal interface GistQueue {
5252
fun fetchUserMessages()
5353
fun logView(message: Message)
5454
fun logOpenedStatus(message: InboxMessage, opened: Boolean)
5555
fun logDeleted(message: InboxMessage)
5656
}
5757

58-
class Queue : GistQueue {
58+
internal class Queue : GistQueue {
5959

6060
private val inAppMessagingManager = SDKComponent.inAppMessagingManager
6161
private val state: InAppMessagingState
@@ -118,12 +118,16 @@ class Queue : GistQueue {
118118
return response
119119
}
120120

121+
// Populates 304 response body with cached data and converts to 200 for Retrofit compatibility.
122+
// Retrofit only populates body() for 2xx responses, so we must convert 304->200.
123+
// Adds custom header to track that this was originally a 304 response.
121124
private fun interceptNotModifiedResponse(response: okhttp3.Response, originalRequest: okhttp3.Request): okhttp3.Response {
122125
val cachedResponse = inAppPreferenceStore.getNetworkResponse(originalRequest.url.toString())
123126
return cachedResponse?.let {
124127
response.newBuilder()
125128
.body(it.toResponseBody(null))
126129
.code(200)
130+
.header(HEADER_FROM_CACHE, "true")
127131
.build()
128132
} ?: response
129133
}
@@ -135,9 +139,14 @@ class Queue : GistQueue {
135139
val latestMessagesResponse = gistQueueService.fetchMessagesForUser(sessionId = state.sessionId)
136140

137141
val code = latestMessagesResponse.code()
142+
val fromCache = latestMessagesResponse.headers()[HEADER_FROM_CACHE] == "true"
138143
when {
139144
(code == 204 || code == 304) -> handleNoContent(code)
140-
latestMessagesResponse.isSuccessful -> handleSuccessfulFetch(latestMessagesResponse.body())
145+
latestMessagesResponse.isSuccessful -> handleSuccessfulFetch(
146+
responseBody = latestMessagesResponse.body(),
147+
fromCache = fromCache
148+
)
149+
141150
else -> handleFailedFetch(code)
142151
}
143152

@@ -154,7 +163,9 @@ class Queue : GistQueue {
154163
inAppMessagingManager.dispatch(InAppMessagingAction.ClearMessageQueue)
155164
}
156165

157-
private fun handleSuccessfulFetch(responseBody: QueueMessagesResponse?) {
166+
// For cached responses (304), apply locally cached opened status to preserve user's changes.
167+
// For fresh responses (200), clear cached status and use server's data.
168+
private fun handleSuccessfulFetch(responseBody: QueueMessagesResponse?, fromCache: Boolean) {
158169
if (responseBody == null) {
159170
logger.error("Received null response body for successful fetch")
160171
return
@@ -184,7 +195,19 @@ class Queue : GistQueue {
184195
// Process inbox messages next
185196
val inboxMessages = response.inboxMessages
186197
logger.debug("Found ${inboxMessages.count()} inbox messages for user")
187-
val inboxMessagesMapped = inboxMessages.mapNotNull { it.toDomain() }
198+
val inboxMessagesMapped = inboxMessages.mapNotNull { item ->
199+
item.toDomain()?.let { message ->
200+
if (fromCache) {
201+
// 304: apply cached opened status if available
202+
val cachedOpenedStatus = inAppPreferenceStore.getInboxMessageOpenedStatus(message.queueId)
203+
return@let cachedOpenedStatus?.let { message.copy(opened = it) } ?: message
204+
} else {
205+
// 200: clear cached status and use server's data
206+
inAppPreferenceStore.clearInboxMessageOpenedStatus(message.queueId)
207+
return@let message
208+
}
209+
}
210+
}
188211
if (inboxMessagesMapped.size < inboxMessages.size) {
189212
logger.debug("Filtered out ${inboxMessages.size - inboxMessagesMapped.size} invalid inbox message(s)")
190213
}
@@ -238,11 +261,14 @@ class Queue : GistQueue {
238261
scope.launch {
239262
try {
240263
logger.debug("Updating inbox message ${message.toLogString()} opened status to: $opened")
264+
// Log updated opened status to server
241265
gistQueueService.logInboxMessageOpened(
242266
queueId = message.queueId,
243267
sessionId = state.sessionId,
244268
body = mapOf("opened" to opened)
245269
)
270+
// Cache the opened status locally for 304 responses
271+
inAppPreferenceStore.saveInboxMessageOpenedStatus(message.queueId, opened)
246272
} catch (e: Exception) {
247273
logger.error("Failed to update inbox message ${message.toLogString()} opened status: ${e.message}")
248274
}
@@ -253,13 +279,21 @@ class Queue : GistQueue {
253279
scope.launch {
254280
try {
255281
logger.debug("Deleting inbox message: ${message.toLogString()}")
282+
// Log deletion event to server
256283
gistQueueService.logUserMessageView(
257284
queueId = message.queueId,
258285
sessionId = state.sessionId
259286
)
287+
// Clear any cached opened status for deleted message
288+
inAppPreferenceStore.clearInboxMessageOpenedStatus(message.queueId)
260289
} catch (e: Exception) {
261290
logger.error("Failed to delete inbox message ${message.toLogString()}: ${e.message}")
262291
}
263292
}
264293
}
294+
295+
companion object {
296+
// Custom header to distinguish 304 responses (converted to 200) from genuine 200 responses
297+
private const val HEADER_FROM_CACHE = "X-CIO-MOBILE-SDK-Cache"
298+
}
265299
}

messaginginapp/src/main/java/io/customer/messaginginapp/store/InAppPreferenceStore.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import androidx.core.content.edit
55
import io.customer.sdk.data.store.PreferenceStore
66
import io.customer.sdk.data.store.read
77

8-
interface InAppPreferenceStore {
8+
internal interface InAppPreferenceStore {
99
fun saveNetworkResponse(url: String, response: String)
1010
fun getNetworkResponse(url: String): String?
1111
fun clearAll()
@@ -27,6 +27,11 @@ interface InAppPreferenceStore {
2727
fun setAnonymousNextShowTime(messageId: String, nextShowTimeMillis: Long)
2828
fun getAnonymousNextShowTime(messageId: String): Long
2929
fun isAnonymousInDelayPeriod(messageId: String): Boolean
30+
31+
// Inbox message opened status caching
32+
fun saveInboxMessageOpenedStatus(queueId: String, opened: Boolean)
33+
fun getInboxMessageOpenedStatus(queueId: String): Boolean?
34+
fun clearInboxMessageOpenedStatus(queueId: String)
3035
}
3136

3237
internal class InAppPreferenceStoreImpl(
@@ -44,6 +49,7 @@ internal class InAppPreferenceStoreImpl(
4449
private const val ANONYMOUS_TIMES_SHOWN_PREFIX = "broadcast_times_shown_"
4550
private const val ANONYMOUS_DISMISSED_PREFIX = "broadcast_dismissed_"
4651
private const val ANONYMOUS_NEXT_SHOW_TIME_PREFIX = "broadcast_next_show_time_"
52+
private const val INBOX_MESSAGE_OPENED_PREFIX = "inbox_message_opened_"
4753
}
4854

4955
override fun saveNetworkResponse(url: String, response: String) = prefs.edit {
@@ -131,4 +137,20 @@ internal class InAppPreferenceStoreImpl(
131137
val nextShowTime = getAnonymousNextShowTime(messageId)
132138
return nextShowTime > 0 && System.currentTimeMillis() < nextShowTime
133139
}
140+
141+
override fun saveInboxMessageOpenedStatus(queueId: String, opened: Boolean) = prefs.edit {
142+
putBoolean("$INBOX_MESSAGE_OPENED_PREFIX$queueId", opened)
143+
}
144+
145+
override fun getInboxMessageOpenedStatus(queueId: String): Boolean? = prefs.read {
146+
if (contains("$INBOX_MESSAGE_OPENED_PREFIX$queueId")) {
147+
getBoolean("$INBOX_MESSAGE_OPENED_PREFIX$queueId", false)
148+
} else {
149+
null
150+
}
151+
}
152+
153+
override fun clearInboxMessageOpenedStatus(queueId: String) = prefs.edit {
154+
remove("$INBOX_MESSAGE_OPENED_PREFIX$queueId")
155+
}
134156
}

messaginginapp/src/test/java/io/customer/messaginginapp/InAppPreferenceStoreTest.kt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,59 @@ class InAppPreferenceStoreTest : IntegrationTest() {
228228
inAppPreferenceStore.isAnonymousInDelayPeriod(messageId) shouldBe false
229229
inAppPreferenceStore.getAnonymousNextShowTime(messageId) shouldBeEqualTo 0
230230
}
231+
232+
// Inbox Message Opened Status Tests
233+
@Test
234+
fun saveInboxMessageOpenedStatus_givenQueueIdAndStatus_expectSavedAndRetrieved() {
235+
val queueId = "queue-123"
236+
237+
// Initially should return null (not cached)
238+
inAppPreferenceStore.getInboxMessageOpenedStatus(queueId) shouldBe null
239+
240+
// Save as opened
241+
inAppPreferenceStore.saveInboxMessageOpenedStatus(queueId, true)
242+
inAppPreferenceStore.getInboxMessageOpenedStatus(queueId) shouldBeEqualTo true
243+
244+
// Save as unopened
245+
inAppPreferenceStore.saveInboxMessageOpenedStatus(queueId, false)
246+
inAppPreferenceStore.getInboxMessageOpenedStatus(queueId) shouldBeEqualTo false
247+
}
248+
249+
@Test
250+
fun getInboxMessageOpenedStatus_givenNonexistentQueueId_expectNull() {
251+
val queueId = "nonexistent-queue"
252+
253+
val status = inAppPreferenceStore.getInboxMessageOpenedStatus(queueId)
254+
status shouldBe null
255+
}
256+
257+
@Test
258+
fun clearInboxMessageOpenedStatus_givenCachedStatus_expectCleared() {
259+
val queueId = "queue-456"
260+
261+
// Save status
262+
inAppPreferenceStore.saveInboxMessageOpenedStatus(queueId, true)
263+
inAppPreferenceStore.getInboxMessageOpenedStatus(queueId) shouldBeEqualTo true
264+
265+
// Clear status
266+
inAppPreferenceStore.clearInboxMessageOpenedStatus(queueId)
267+
inAppPreferenceStore.getInboxMessageOpenedStatus(queueId) shouldBe null
268+
}
269+
270+
@Test
271+
fun saveInboxMessageOpenedStatus_givenMultipleMessages_expectEachTrackedIndependently() {
272+
val queueId1 = "queue-1"
273+
val queueId2 = "queue-2"
274+
val queueId3 = "queue-3"
275+
276+
// Save different statuses for different messages
277+
inAppPreferenceStore.saveInboxMessageOpenedStatus(queueId1, true)
278+
inAppPreferenceStore.saveInboxMessageOpenedStatus(queueId2, false)
279+
// Don't save anything for queueId3
280+
281+
// Verify each is tracked correctly
282+
inAppPreferenceStore.getInboxMessageOpenedStatus(queueId1) shouldBeEqualTo true
283+
inAppPreferenceStore.getInboxMessageOpenedStatus(queueId2) shouldBeEqualTo false
284+
inAppPreferenceStore.getInboxMessageOpenedStatus(queueId3) shouldBe null
285+
}
231286
}

0 commit comments

Comments
 (0)