mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 11:18:24 +00:00
fix(nip17): treat text-less renames as system rows in grouping, previews and notifications
A rename that carries only a subject now renders as a system line, but the rest of the app still treated it as a regular message: - Grouping: the author's next message joined the rename's run, so it lost its author line in groups and drew a squared top corner against the pill. Renames no longer join author runs. - Room list: the preview read "You: " or nothing. It now says who renamed the conversation and to what (ChatPreview.SubjectChange). - Notifications: a received rename posted an empty body. It now reads "Renamed the conversation to X". - The feed gives rename rows their own LazyColumn content type, so slots aren't recycled between the pill and the bubble layout. The predicate moves to commons (isSubjectOnlyChatMessage) so all of these share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMJHLxbswamX91WDaPT2uR
This commit is contained in:
+12
-1
@@ -24,7 +24,11 @@ import android.content.Context
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.privateChats.isSubjectOnlyChatMessage
|
||||
import com.vitorpamplona.amethyst.commons.notifications.NotificationContent
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.chat_notification_renamed_conversation_to
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationCategory
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationEnricher
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationRoutes
|
||||
@@ -33,6 +37,7 @@ import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.ReplyA
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.postConversation
|
||||
import com.vitorpamplona.amethyst.service.notifications.notificationManager
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
@@ -94,7 +99,13 @@ object DirectMessageNotification {
|
||||
if (decrypt) {
|
||||
NotificationContent.decryptContent(chatNote, account.signer) ?: return
|
||||
} else {
|
||||
chatNote.event?.content ?: return
|
||||
val event = chatNote.event ?: return
|
||||
// A rename with no text would post an empty notification; say what it did instead.
|
||||
if (event.isSubjectOnlyChatMessage()) {
|
||||
loadStringRes(Res.string.chat_notification_renamed_conversation_to, event.subject() ?: "")
|
||||
} else {
|
||||
event.content
|
||||
}
|
||||
}
|
||||
|
||||
val accountNpub = NotificationRoutes.accountNpub(account)
|
||||
|
||||
+19
@@ -23,6 +23,8 @@ package com.vitorpamplona.amethyst.commons.model.privateChats
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateReplaceableTagArrayEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
@@ -58,6 +60,16 @@ fun Event.hasEncryptedContent(): Boolean =
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* A NIP-17 message that only renames the conversation: it carries a `subject` tag and no text.
|
||||
* It has nothing to show as a message body, so the feed, the room list and the notification each
|
||||
* narrate it as a rename instead. A rename sent with an explanation is a regular message.
|
||||
*
|
||||
* Checks the content first: almost every message has text, and `isBlank` stops at its first
|
||||
* non-space character, so the tag scan only runs for the rare empty one.
|
||||
*/
|
||||
fun Event.isSubjectOnlyChatMessage(): Boolean = this is ChatMessageEvent && content.isBlank() && subject() != null
|
||||
|
||||
/**
|
||||
* What a chat row should render for a message, once the raw ciphertext is off the table.
|
||||
*
|
||||
@@ -70,6 +82,11 @@ sealed interface ChatPreview {
|
||||
val text: String,
|
||||
) : ChatPreview
|
||||
|
||||
/** A message that only renamed the conversation ([isSubjectOnlyChatMessage]) to [subject]. */
|
||||
data class SubjectChange(
|
||||
val subject: String,
|
||||
) : ChatPreview
|
||||
|
||||
/** Encrypted, decryptable by this account, plaintext not available yet. */
|
||||
data object Decrypting : ChatPreview
|
||||
|
||||
@@ -97,6 +114,8 @@ fun chatPreviewOf(
|
||||
): ChatPreview {
|
||||
if (event == null) return ChatPreview.Missing
|
||||
|
||||
if (event.isSubjectOnlyChatMessage()) return ChatPreview.SubjectChange(event.subject() ?: "")
|
||||
|
||||
if (!event.hasEncryptedContent()) return ChatPreview.Body(decrypted ?: event.content)
|
||||
|
||||
// Never trust `event.content` from here down: it is ciphertext.
|
||||
|
||||
+44
@@ -59,6 +59,50 @@ class ChatPreviewTest {
|
||||
sig = someSig,
|
||||
)
|
||||
|
||||
private fun nip17Rename(content: String = "") =
|
||||
ChatMessageEvent(
|
||||
id = "rn".padEnd(64, '0'),
|
||||
pubKey = other,
|
||||
createdAt = 1_000,
|
||||
tags = arrayOf(arrayOf("p", me), arrayOf("subject", "Weekend plans")),
|
||||
content = content,
|
||||
sig = someSig,
|
||||
)
|
||||
|
||||
// ---- subject-only renames ------------------------------------------------
|
||||
|
||||
@Test
|
||||
fun renameWithoutTextIsSubjectOnly() {
|
||||
assertTrue(nip17Rename().isSubjectOnlyChatMessage())
|
||||
assertTrue(nip17Rename(content = " \n").isSubjectOnlyChatMessage())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun renameWithTextIsARegularMessage() {
|
||||
assertFalse(nip17Rename(content = "new goals").isSubjectOnlyChatMessage())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageWithoutSubjectIsNotARename() {
|
||||
assertFalse(nip17Rumor().isSubjectOnlyChatMessage())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun renamePreviewNamesTheNewSubject() {
|
||||
assertEquals(
|
||||
ChatPreview.SubjectChange("Weekend plans"),
|
||||
chatPreviewOf(nip17Rename(), decrypted = null, myPubKey = me, canDecrypt = true),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun renameWithTextPreviewsTheText() {
|
||||
assertEquals(
|
||||
ChatPreview.Body("new goals"),
|
||||
chatPreviewOf(nip17Rename(content = "new goals"), decrypted = null, myPubKey = me, canDecrypt = true),
|
||||
)
|
||||
}
|
||||
|
||||
// ---- hasEncryptedContent -------------------------------------------------
|
||||
|
||||
@Test
|
||||
|
||||
@@ -2327,6 +2327,7 @@
|
||||
<string name="chat_system_renamed_conversation_you">You renamed the conversation</string>
|
||||
<string name="chat_system_renamed_conversation_to">%1$s renamed the conversation to %2$s</string>
|
||||
<string name="chat_system_renamed_conversation_to_you">You renamed the conversation to %1$s</string>
|
||||
<string name="chat_notification_renamed_conversation_to">Renamed the conversation to %1$s</string>
|
||||
<string name="buzz_message_edited">(edited)</string>
|
||||
<string name="buzz_diff_truncated">(diff truncated)</string>
|
||||
<string name="buzz_system_member_joined">%1$s joined</string>
|
||||
|
||||
+5
-2
@@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.privateChats.isSubjectOnlyChatMessage
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.dateFormatter
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeMe
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleShapeMeBottom
|
||||
@@ -92,7 +93,8 @@ const val CHAT_GROUP_WINDOW_SECONDS = 10 * 60L
|
||||
|
||||
/**
|
||||
* Event kinds that don't render as regular bubbles (zaps, raids, clips) or that
|
||||
* read as system messages (channel admin events) never join an author run.
|
||||
* read as system messages (channel admin events, NIP-17 renames) never join an
|
||||
* author run.
|
||||
*/
|
||||
private fun isGroupableEvent(event: Event?): Boolean =
|
||||
event != null &&
|
||||
@@ -100,7 +102,8 @@ private fun isGroupableEvent(event: Event?): Boolean =
|
||||
event !is LiveActivitiesRaidEvent &&
|
||||
event !is LiveActivitiesClipEvent &&
|
||||
event !is ChannelCreateEvent &&
|
||||
event !is ChannelMetadataEvent
|
||||
event !is ChannelMetadataEvent &&
|
||||
!event.isSubjectOnlyChatMessage()
|
||||
|
||||
/**
|
||||
* Whether [newer] continues the author run started by [older]. Mirrors the break
|
||||
|
||||
+15
-1
@@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.DraftTagState
|
||||
import com.vitorpamplona.amethyst.commons.model.privateChats.isSubjectOnlyChatMessage
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError
|
||||
@@ -55,6 +56,19 @@ import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private const val SUBJECT_ONLY_CONTENT_TYPE = -2
|
||||
|
||||
/**
|
||||
* The lazy-list content type of a chat row: its event kind, except that a NIP-17 rename with no
|
||||
* text gets its own type. It renders as a system pill rather than a bubble, so letting it share
|
||||
* kind 14's slot with ordinary messages would make the list rebuild one layout into the other
|
||||
* whenever a recycled slot crossed between the two.
|
||||
*/
|
||||
private fun chatRowContentType(item: Note): Int {
|
||||
val event = item.event ?: return -1
|
||||
return if (event.isSubjectOnlyChatMessage()) SUBJECT_ONLY_CONTENT_TYPE else event.kind
|
||||
}
|
||||
|
||||
/**
|
||||
* A caller's own rendering for feed rows that are not chat bubbles.
|
||||
*
|
||||
@@ -245,7 +259,7 @@ fun ChatFeedLoaded(
|
||||
reverseLayout = true,
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(items.list, key = { _, item -> item.idHex }, contentType = { _, item -> item.event?.kind ?: -1 }) { index, item ->
|
||||
itemsIndexed(items.list, key = { _, item -> item.idHex }, contentType = { _, item -> chatRowContentType(item) }) { index, item ->
|
||||
val noteEvent = item.event
|
||||
if (avoidDraft == null || noteEvent !is DraftWrapEvent || noteEvent.dTag() !in avoidDraft.usedDraftTags) {
|
||||
// Reverse layout: index - 1 is the newer message (visually below),
|
||||
|
||||
+2
-2
@@ -50,6 +50,7 @@ import com.vitorpamplona.amethyst.commons.chats.ui.ChatGroupPosition
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.jumboEmojiCount
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.routeFor
|
||||
import com.vitorpamplona.amethyst.commons.model.privateChats.isSubjectOnlyChatMessage
|
||||
import com.vitorpamplona.amethyst.commons.notifications.NotificationContent
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LocalInlineQuoteRenderer
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
@@ -79,7 +80,6 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.Re
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.hasEncryptedMediaV2
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.hasMip04Media
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.isBuzzActivityRow
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.types.isSubjectOnlyChatMessage
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ReactionRowZapraiser
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
@@ -180,7 +180,7 @@ fun ChatroomMessageCompose(
|
||||
} else if (event is ConcordTimerNoticeEvent) {
|
||||
// Concord kind-1740: "Alice set disappearing messages to 30 days" (CORD-08 §4).
|
||||
RenderConcordTimerNotice(baseNote, accountViewModel, nav)
|
||||
} else if (isSubjectOnlyChatMessage(event)) {
|
||||
} else if (event?.isSubjectOnlyChatMessage() == true) {
|
||||
// NIP-17 rename with no text: a system line under the subject divider, not an empty bubble.
|
||||
RenderChatSubjectChange(baseNote, innerQuote, accountViewModel, nav)
|
||||
} else if (isBuzzActivityRow(event)) {
|
||||
|
||||
+3
-9
@@ -25,6 +25,7 @@ import androidx.compose.runtime.remember
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.ChatSystemMessage
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.model.privateChats.isSubjectOnlyChatMessage
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.chat_system_renamed_conversation
|
||||
import com.vitorpamplona.amethyst.commons.resources.chat_system_renamed_conversation_to
|
||||
@@ -36,20 +37,13 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.ChatTime
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size18dp
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip14Subject.subject
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
|
||||
/**
|
||||
* A NIP-17 message that only renames the conversation: it carries a `subject` tag and no text.
|
||||
* The room's subject divider already announces the new name above it, so drawing it as a bubble
|
||||
* left an empty balloon under the divider whose only content was the time.
|
||||
*/
|
||||
fun isSubjectOnlyChatMessage(event: Event?): Boolean = event is ChatMessageEvent && event.content.isBlank() && event.subject() != null
|
||||
|
||||
/**
|
||||
* A rename ([isSubjectOnlyChatMessage]) as a centered system line — "Alice renamed the
|
||||
* conversation" with Alice's avatar — under the subject divider that carries the new name.
|
||||
* conversation" with Alice's avatar — under the subject divider that carries the new name. The
|
||||
* divider alone left an empty bubble below it whose only content was the time.
|
||||
*
|
||||
* Unlike the other system lines this one is a real message with a sender and a delivery, so the
|
||||
* pill keeps the bubble's tappable time and, on our own renames, its relay-acceptance ticks.
|
||||
|
||||
+11
@@ -92,6 +92,8 @@ import com.vitorpamplona.amethyst.commons.resources.channel_invite_row_added_you
|
||||
import com.vitorpamplona.amethyst.commons.resources.channel_invite_row_added_you_by
|
||||
import com.vitorpamplona.amethyst.commons.resources.chat_preview_decrypting
|
||||
import com.vitorpamplona.amethyst.commons.resources.chat_preview_you_prefix
|
||||
import com.vitorpamplona.amethyst.commons.resources.chat_system_renamed_conversation_to
|
||||
import com.vitorpamplona.amethyst.commons.resources.chat_system_renamed_conversation_to_you
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_direct_invite_from
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_direct_invite_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_home_title
|
||||
@@ -1325,6 +1327,15 @@ private fun RowScope.LastMessagePreview(
|
||||
val sentByMe = lastMessage.author?.pubkeyHex == accountViewModel.account.signer.pubKey
|
||||
if (sentByMe) stringRes(Res.string.chat_preview_you_prefix, preview.text) else preview.text
|
||||
}
|
||||
is ChatPreview.SubjectChange -> {
|
||||
// A rename with no text has no body to quote; say what it did instead of "You: ".
|
||||
val authorHex = lastMessage.event?.pubKey
|
||||
if (authorHex == accountViewModel.account.signer.pubKey) {
|
||||
stringRes(Res.string.chat_system_renamed_conversation_to_you, preview.subject)
|
||||
} else {
|
||||
stringRes(Res.string.chat_system_renamed_conversation_to, observeUserNameByHex(authorHex, accountViewModel), preview.subject)
|
||||
}
|
||||
}
|
||||
ChatPreview.Decrypting -> stringRes(Res.string.chat_preview_decrypting)
|
||||
ChatPreview.Undecryptable -> stringRes(Res.string.could_not_decrypt_the_message)
|
||||
ChatPreview.Missing -> stringRes(Res.string.referenced_event_not_found)
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.chats.ui
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.UserContext
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* A NIP-17 rename with no text renders as a system line, not a bubble, so it must not join the
|
||||
* author run of the bubbles around it — otherwise the author's next message is drawn as the
|
||||
* continuation of a bubble that isn't there: no author line, and a squared top corner.
|
||||
*/
|
||||
class ChatGroupPositionTest {
|
||||
private val alice = "a".repeat(64)
|
||||
private val bob = "b".repeat(64)
|
||||
private val context = UserContext { addr -> AddressableNote(addr) }
|
||||
|
||||
private fun message(
|
||||
id: Char,
|
||||
createdAt: Long,
|
||||
content: String,
|
||||
subject: String? = null,
|
||||
): Note {
|
||||
val tags = listOfNotNull(arrayOf("p", bob), subject?.let { arrayOf("subject", it) }).toTypedArray()
|
||||
val event = ChatMessageEvent(id.toString().repeat(64), alice, createdAt, tags, content, "")
|
||||
return Note(event.id).also { it.loadEvent(event, User(alice, context), emptyList()) }
|
||||
}
|
||||
|
||||
private val start = 1_800_000_000L
|
||||
|
||||
@Test
|
||||
fun messageAfterARenameStartsANewRun() {
|
||||
val rename = message('1', start, content = "", subject = "Weekend plans")
|
||||
val next = message('2', start + 30, content = "hi all")
|
||||
val after = message('3', start + 60, content = "who's in?")
|
||||
|
||||
assertEquals(ChatGroupPosition.SINGLE, computeChatGroupPosition(next, rename, null))
|
||||
assertEquals(ChatGroupPosition.TOP, computeChatGroupPosition(after, next, rename))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageBeforeARenameEndsItsRun() {
|
||||
val first = message('1', start, content = "hey")
|
||||
val before = message('2', start + 30, content = "one sec")
|
||||
val rename = message('3', start + 60, content = "", subject = "Weekend plans")
|
||||
|
||||
assertEquals(ChatGroupPosition.BOTTOM, computeChatGroupPosition(rename, before, first))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun renameWithTextStillLeadsTheRunAfterIt() {
|
||||
val rename = message('1', start, content = "new goals", subject = "Weekend plans")
|
||||
val next = message('2', start + 30, content = "hi all")
|
||||
|
||||
assertEquals(ChatGroupPosition.TOP, computeChatGroupPosition(next, rename, null))
|
||||
assertEquals(ChatGroupPosition.BOTTOM, computeChatGroupPosition(null, next, rename))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user