Skip to content

Commit 0cc6ee6

Browse files
committed
fix(messenger): hold a message-link landing until the linked group resolves
A link with a group id could open with the chat marked a tip DM before the group loaded, so the landing read as Readable and paged a withheld group's history. A group hydrated by id also has a null membership, which left the landing waiting forever. Carry linkedAsGroup from the route and read a null membership as outside, as the gate does.
1 parent dfc1dab commit 0cc6ee6

3 files changed

Lines changed: 59 additions & 4 deletions

File tree

‎apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,14 @@ fun ChatFlowScreen(
7777
// first frame already has what the open can draw from memory (a group's name and picture). The
7878
// steps get this instance from flowSharedViewModel: the flow's store owner is this entry's.
7979
val viewModel = hiltViewModel<ChatViewModel, ChatViewModel.Factory>(
80-
creationCallback = { factory -> factory.create(route.identifier, route.jumpToMessageId, route.returnToTitle) },
80+
creationCallback = { factory ->
81+
factory.create(
82+
route.identifier,
83+
route.jumpToMessageId,
84+
route.returnToTitle,
85+
linkedAsGroup = route.jumpToMessageId != null && route.isGroupLink,
86+
)
87+
},
8188
)
8289

8390
// A group link the server has no record of: say so and leave, rather than sit on an empty chat

‎apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,11 @@ internal class ChatViewModel @AssistedInject constructor(
253253
* back to it. Null for any other open.
254254
*/
255255
@Assisted private val returnToTitle: String? = null,
256+
/**
257+
* Whether the link that opened this visit named a group (a dashed UUID id) rather than a DM.
258+
* See [State.linkedAsGroup].
259+
*/
260+
@Assisted private val linkedAsGroup: Boolean = false,
256261
private val chatCoordinator: ChatCoordinator,
257262
private val mediaUploads: ChatMediaUploads,
258263
private val e2eePolicy: E2eePolicy,
@@ -292,6 +297,7 @@ internal class ChatViewModel @AssistedInject constructor(
292297
initialState = State(
293298
openingAtMessage = openAtMessageId != null,
294299
returnToTitle = returnToTitle,
300+
linkedAsGroup = linkedAsGroup,
295301
),
296302
updateStateForEvent = updateStateForEvent,
297303
defaultDispatcher = dispatchers.Default,
@@ -441,6 +447,12 @@ internal class ChatViewModel @AssistedInject constructor(
441447
* the message away or sending.
442448
*/
443449
val returnToTitle: String? = null,
450+
/**
451+
* Whether a message link opened this visit with a group's id. The open can mark the chat a
452+
* tip DM before the group resolves, and a landing that believed it would page a withheld
453+
* group's history, so [messageLinkLanding] waits on the group for such a link.
454+
*/
455+
val linkedAsGroup: Boolean = false,
444456
/**
445457
* Where the "N Unread Messages" divider sits, read once per visit from the viewer's stored
446458
* READ pointer. Never re-read: the list advances that pointer as the reader scrolls, and a
@@ -641,15 +653,19 @@ internal class ChatViewModel @AssistedInject constructor(
641653
val messageLinkLanding: MessageLinkLanding?
642654
get() = when {
643655
chatNotFound -> MessageLinkLanding.Withheld
656+
// A null membership is a group hydrated by id, which the device holds no row for;
657+
// like the gate, read it as outside (see ChatMembership.isMember).
644658
subject is ChatSubject.Group -> when (subject.isMember) {
645659
true -> MessageLinkLanding.Readable
646-
null -> null
647-
false -> when (groupAccess) {
660+
null, false -> when (groupAccess) {
648661
null, GroupAccess.Undetermined -> null
649662
else -> if (readsFromOutside) MessageLinkLanding.Readable
650663
else MessageLinkLanding.Withheld
651664
}
652665
}
666+
// The open can call a group a tip DM before the group resolves, so a link that
667+
// named a group never reads as one.
668+
linkedAsGroup -> null
653669
chatType == ChatType.TIP_DM || chatType == ChatType.CONTACT_DM ->
654670
MessageLinkLanding.Readable
655671
else -> null
@@ -3478,6 +3494,7 @@ internal class ChatViewModel @AssistedInject constructor(
34783494
identifier: ChatIdentifier?,
34793495
openAtMessageId: Long?,
34803496
returnToTitle: String?,
3497+
linkedAsGroup: Boolean,
34813498
): ChatViewModel
34823499
}
34833500

‎apps/flipcash/features/messenger/src/test/kotlin/com/flipcash/app/messenger/internal/ChatViewModelStateTest.kt‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,6 @@ class ChatViewModelStateTest {
663663
fun `a message link waits while the viewer's standing in a group is unknown`() {
664664
// A chat id alone, before the open says what it is: paging now could fetch a withheld group.
665665
assertNull(ChatViewModel.State(chatId = groupChatId).messageLinkLanding)
666-
assertNull(ChatViewModel.State(subject = group(isMember = null)).messageLinkLanding)
667666
assertNull(ChatViewModel.State(subject = group(isMember = false)).messageLinkLanding)
668667
assertNull(
669668
ChatViewModel.State(
@@ -673,6 +672,38 @@ class ChatViewModelStateTest {
673672
)
674673
}
675674

675+
@Test
676+
fun `a message link into a group hydrated by id waits on access, not membership`() {
677+
// A group the device holds no row for resolves with isMember = null and keeps it, so
678+
// waiting on membership would hold the landing forever; seen on device.
679+
assertNull(ChatViewModel.State(subject = group(isMember = null)).messageLinkLanding)
680+
assertEquals(
681+
MessageLinkLanding.Withheld,
682+
ChatViewModel.State(
683+
subject = group(isMember = null),
684+
groupAccess = GroupAccess.Blocked(unmet),
685+
).messageLinkLanding,
686+
)
687+
}
688+
689+
@Test
690+
fun `a group's message link waits for the group even after the open calls it a tip DM`() {
691+
// OnTipDmDetected can mark the chat a tip DM before the group resolves; seen on device as
692+
// a gated group's history being paged.
693+
assertNull(
694+
ChatViewModel.State(linkedAsGroup = true, chatType = ChatType.TIP_DM).messageLinkLanding
695+
)
696+
assertEquals(
697+
MessageLinkLanding.Withheld,
698+
ChatViewModel.State(
699+
linkedAsGroup = true,
700+
chatType = ChatType.GROUP,
701+
subject = group(isMember = false),
702+
groupAccess = GroupAccess.Blocked(unmet),
703+
).messageLinkLanding,
704+
)
705+
}
706+
676707
@Test
677708
fun `a message link lands where the transcript is the viewer's to read`() {
678709
assertEquals(

0 commit comments

Comments
 (0)