mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-28 19:58:16 +00:00
core, ui: chat banner (#6089)
* core: create banner item
* filter deletions
* fix query
* ios
* fixes
* remove comment
* revert diff
* refactor
* fix most tests
* fix tests
* spacer
* plans
* create banner for 1-time link initiator
* style in progress
* change background
* ui
* remove bio length limit
* ui
* create banner for client chat
* rename
* more contexts
* fix tests
* move
* fixed image size
* plans
* remove diff
* kotlin
* copy
* paddings
* paddings
* comment
* layout, messages
* fonts
* texts, icons
* kotlin refactor
* kotlin texts
* fix date
* Revert "fix date"
This reverts commit abbd48b334.
* date
* fix texts
* kotlin date
* color and corners
* kotlin
* color
* update banner, context menu in ios
* update texts, do not show epoch timestamp for banner
* fix texts
---------
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
co-authored by
Evgeny Poberezkin
parent
ec061bcbb9
commit
68b7f09c8f
@@ -2813,6 +2813,7 @@ data class ChatItem (
|
||||
is CIContent.RcvDirectE2EEInfo -> false
|
||||
is CIContent.SndGroupE2EEInfo -> false
|
||||
is CIContent.RcvGroupE2EEInfo -> false
|
||||
is CIContent.ChatBanner -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
@@ -2879,6 +2880,7 @@ data class ChatItem (
|
||||
is CIContent.RcvDirectE2EEInfo -> false
|
||||
is CIContent.SndGroupE2EEInfo -> false
|
||||
is CIContent.RcvGroupE2EEInfo -> false
|
||||
is CIContent.ChatBanner -> false
|
||||
is CIContent.InvalidJSON -> false
|
||||
}
|
||||
|
||||
@@ -3549,6 +3551,7 @@ sealed class CIContent: ItemContent {
|
||||
@Serializable @SerialName("rcvDirectE2EEInfo") class RcvDirectE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndGroupE2EEInfo") class SndGroupE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvGroupE2EEInfo") class RcvGroupE2EEInfo(val e2eeInfo: E2EEInfo): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("chatBanner") object ChatBanner: CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
|
||||
override val text: String get() = when (this) {
|
||||
@@ -3582,6 +3585,7 @@ sealed class CIContent: ItemContent {
|
||||
is RcvDirectE2EEInfo -> directE2EEInfoStr(e2eeInfo)
|
||||
is SndGroupE2EEInfo -> e2eeInfoNoPQStr
|
||||
is RcvGroupE2EEInfo -> e2eeInfoNoPQStr
|
||||
is ChatBanner -> ""
|
||||
is InvalidJSON -> "invalid data"
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -883,7 +883,7 @@ object ChatController {
|
||||
}
|
||||
|
||||
suspend fun apiStartChat(ctrl: ChatCtrl? = null): Boolean {
|
||||
val r = sendCmd(null, CC.StartChat(mainApp = true, largeLinkData = false), ctrl)
|
||||
val r = sendCmd(null, CC.StartChat(mainApp = true, largeLinkData = true), ctrl)
|
||||
when (r.result) {
|
||||
is CR.ChatStarted -> return true
|
||||
is CR.ChatRunning -> return false
|
||||
|
||||
+181
-33
@@ -42,7 +42,9 @@ import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.platform.AudioPlayer
|
||||
import chat.simplex.common.views.newchat.ContactConnectionInfoView
|
||||
import chat.simplex.common.views.newchat.alertProfileImageSize
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.*
|
||||
@@ -1754,6 +1756,127 @@ fun BoxScope.ChatItemsList(
|
||||
ChatItemView(cItem, range, itemSeparation, previousItemSeparationLargeGap)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatBannerView() {
|
||||
fun chatContext(): String? {
|
||||
return when (chatInfo) {
|
||||
is ChatInfo.Direct -> {
|
||||
val contact = chatInfo.contact
|
||||
val preparedLinkType = contact.preparedContact?.uiConnLinkType
|
||||
if (contact.nextConnectPrepared && preparedLinkType != null) {
|
||||
when (preparedLinkType) {
|
||||
ConnectionMode.Inv -> generalGetString(MR.strings.chat_banner_connect_to_chat)
|
||||
ConnectionMode.Con -> generalGetString(MR.strings.chat_banner_send_request_to_connect)
|
||||
}
|
||||
} else if (contact.nextAcceptContactRequest) {
|
||||
generalGetString(MR.strings.chat_banner_accept_contact_request)
|
||||
} else {
|
||||
generalGetString(MR.strings.chat_banner_your_contact)
|
||||
}
|
||||
}
|
||||
|
||||
is ChatInfo.Group -> {
|
||||
val groupInfo = chatInfo.groupInfo
|
||||
when (groupInfo.businessChat?.chatType) {
|
||||
null -> {
|
||||
if (groupInfo.nextConnectPrepared) {
|
||||
generalGetString(MR.strings.chat_banner_join_group)
|
||||
} else {
|
||||
when (groupInfo.membership.memberStatus) {
|
||||
GroupMemberStatus.MemInvited -> generalGetString(MR.strings.chat_banner_join_group)
|
||||
GroupMemberStatus.MemCreator -> generalGetString(MR.strings.chat_banner_your_group)
|
||||
else -> generalGetString(MR.strings.chat_banner_group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BusinessChatType.Business ->
|
||||
if (groupInfo.nextConnectPrepared) {
|
||||
generalGetString(MR.strings.chat_banner_connect_to_chat)
|
||||
} else {
|
||||
generalGetString(MR.strings.chat_banner_business_connection)
|
||||
}
|
||||
BusinessChatType.Customer ->
|
||||
generalGetString(MR.strings.chat_banner_your_business_contact)
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.clipChatItem()
|
||||
.background(MaterialTheme.appColors.receivedMessage)
|
||||
) {
|
||||
val bannerModifier = if (appPlatform.isDesktop) Modifier.width(400.dp) else Modifier.fillMaxWidth()
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = bannerModifier
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
.padding(bottom = DEFAULT_PADDING)
|
||||
// ChatInfoImage has its own padding somewhere,
|
||||
// also not doing verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF) because of it
|
||||
.padding(top = DEFAULT_PADDING_HALF)
|
||||
.background(MaterialTheme.appColors.receivedMessage)
|
||||
) {
|
||||
ChatInfoImage(chatInfo, size = alertProfileImageSize, iconColor = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f))
|
||||
Text(
|
||||
chatInfo.displayName,
|
||||
style = MaterialTheme.typography.h3,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.widthIn(max = 240.dp)
|
||||
)
|
||||
|
||||
val fullName = chatInfo.fullName.trim()
|
||||
if (fullName.isNotEmpty() && fullName != chatInfo.displayName && fullName != chatInfo.displayName.trim()) {
|
||||
Text(
|
||||
fullName,
|
||||
style = MaterialTheme.typography.h4,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.widthIn(max = 260.dp)
|
||||
.padding(top = DEFAULT_PADDING_HALF)
|
||||
)
|
||||
}
|
||||
|
||||
val descr = chatInfo.shortDescr?.trim()
|
||||
if (descr != null && descr != "") {
|
||||
Text(
|
||||
descr,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
lineHeight = 21.sp,
|
||||
modifier = Modifier
|
||||
.padding(top = DEFAULT_PADDING_HALF)
|
||||
)
|
||||
}
|
||||
|
||||
val contextStr = chatContext()
|
||||
if (contextStr != null) {
|
||||
Text(
|
||||
contextStr,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.padding(top = DEFAULT_PADDING)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
Modifier.align(Alignment.BottomCenter),
|
||||
state = listState.value,
|
||||
@@ -1768,42 +1891,62 @@ fun BoxScope.ChatItemsList(
|
||||
) {
|
||||
val mergedItemsValue = mergedItems.value
|
||||
itemsIndexed(mergedItemsValue.items, key = { _, merged -> keyForItem(merged.newest().item) }) { index, merged ->
|
||||
val isLastItem = index == mergedItemsValue.items.lastIndex
|
||||
val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null
|
||||
val listItem = merged.newest()
|
||||
val item = listItem.item
|
||||
val range = if (merged is MergedItem.Grouped) {
|
||||
merged.rangeInReversed.value
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val showAvatar = shouldShowAvatar(item, merged.oldest().nextItem)
|
||||
val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } }
|
||||
val itemSeparation: ItemSeparation
|
||||
val prevItemSeparationLargeGap: Boolean
|
||||
if (merged is MergedItem.Single || isRevealed.value) {
|
||||
val prev = listItem.prevItem
|
||||
itemSeparation = getItemSeparation(item, prev)
|
||||
val nextForGap = if ((item.mergeCategory != null && item.mergeCategory == prev?.mergeCategory) || isLastItem) null else listItem.nextItem
|
||||
prevItemSeparationLargeGap = if (nextForGap == null) false else getItemSeparationLargeGap(nextForGap, item)
|
||||
} else {
|
||||
itemSeparation = getItemSeparation(item, null)
|
||||
prevItemSeparationLargeGap = false
|
||||
}
|
||||
ChatViewListItem(index == 0, rememberUpdatedState(range), showAvatar, item, itemSeparation, prevItemSeparationLargeGap, isRevealed) {
|
||||
if (merged is MergedItem.Grouped) merged.reveal(it, revealedItems)
|
||||
}
|
||||
|
||||
if (last != null) {
|
||||
// no using separate item(){} block in order to have total number of items in LazyColumn match number of merged items
|
||||
DateSeparator(last.meta.itemTs)
|
||||
}
|
||||
if (item.isRcvNew) {
|
||||
val itemIds = when (merged) {
|
||||
is MergedItem.Single -> listOf(merged.item.item.id)
|
||||
is MergedItem.Grouped -> merged.items.map { it.item.id }
|
||||
if (item.content is CIContent.ChatBanner) {
|
||||
Column {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
.padding(bottom = 90.dp, top = DEFAULT_PADDING)
|
||||
) {
|
||||
ChatBannerView()
|
||||
}
|
||||
|
||||
val prevItem = listItem.prevItem
|
||||
if (prevItem != null) {
|
||||
DateSeparator(prevItem.meta.itemTs)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val isLastItem = index == mergedItemsValue.items.lastIndex
|
||||
val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null
|
||||
val range = if (merged is MergedItem.Grouped) {
|
||||
merged.rangeInReversed.value
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val showAvatar = shouldShowAvatar(item, merged.oldest().nextItem)
|
||||
val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } }
|
||||
val itemSeparation: ItemSeparation
|
||||
val prevItemSeparationLargeGap: Boolean
|
||||
if (merged is MergedItem.Single || isRevealed.value) {
|
||||
val prev = listItem.prevItem
|
||||
itemSeparation = getItemSeparation(item, prev)
|
||||
val nextForGap = if ((item.mergeCategory != null && item.mergeCategory == prev?.mergeCategory) || isLastItem) null else listItem.nextItem
|
||||
prevItemSeparationLargeGap = if (nextForGap == null) false else getItemSeparationLargeGap(nextForGap, item)
|
||||
} else {
|
||||
itemSeparation = getItemSeparation(item, null)
|
||||
prevItemSeparationLargeGap = false
|
||||
}
|
||||
ChatViewListItem(index == 0, rememberUpdatedState(range), showAvatar, item, itemSeparation, prevItemSeparationLargeGap, isRevealed) {
|
||||
if (merged is MergedItem.Grouped) merged.reveal(it, revealedItems)
|
||||
}
|
||||
|
||||
if (last != null) {
|
||||
// no using separate item(){} block in order to have total number of items in LazyColumn match number of merged items
|
||||
DateSeparator(last.meta.itemTs)
|
||||
}
|
||||
if (item.isRcvNew) {
|
||||
val itemIds = when (merged) {
|
||||
is MergedItem.Single -> listOf(merged.item.item.id)
|
||||
is MergedItem.Grouped -> merged.items.map { it.item.id }
|
||||
}
|
||||
MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead)
|
||||
}
|
||||
MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2256,7 +2399,12 @@ private fun FloatingDate(
|
||||
if (listState.value.layoutInfo.visibleItemsInfo.lastIndex >= 0) {
|
||||
val lastVisibleChatItem = lastFullyVisibleIemInListState(topPaddingToContentPx, density, fontSizeSqrtMultiplier, mergedItems, listState)
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
lastVisibleChatItem?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone)
|
||||
val itemTs = lastVisibleChatItem?.meta?.itemTs
|
||||
if (itemTs != null && itemTs.epochSeconds > 0) {
|
||||
itemTs.toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
+1
@@ -787,6 +787,7 @@ fun ChatItemView(
|
||||
is CIContent.RcvDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo)
|
||||
is CIContent.SndGroupE2EEInfo -> E2EEInfoNoPQText()
|
||||
is CIContent.RcvGroupE2EEInfo -> E2EEInfoNoPQText()
|
||||
is CIContent.ChatBanner -> Spacer(modifier = Modifier.size(0.dp))
|
||||
is CIContent.InvalidJSON -> {
|
||||
CIInvalidJSONView(c.json)
|
||||
DeleteItemMenu()
|
||||
|
||||
+4
-1
@@ -192,7 +192,10 @@ fun ChatPreviewView(
|
||||
|
||||
is ChatInfo.Group ->
|
||||
if (cInfo.groupInfo.nextConnectPrepared) {
|
||||
stringResource(MR.strings.group_preview_open_to_join) to Color.Unspecified
|
||||
stringResource(
|
||||
if (cInfo.groupInfo.businessChat?.chatType == BusinessChatType.Business) MR.strings.open_to_connect
|
||||
else MR.strings.group_preview_open_to_join
|
||||
) to Color.Unspecified
|
||||
} else {
|
||||
when (cInfo.groupInfo.membership.memberStatus) {
|
||||
GroupMemberStatus.MemRejected -> stringResource(MR.strings.group_preview_rejected) to Color.Unspecified
|
||||
|
||||
@@ -485,6 +485,17 @@
|
||||
<string name="group_new_support_chat_one">1 chat with a member</string>
|
||||
<string name="group_new_support_chats_short">%d chat(s)</string>
|
||||
|
||||
<!-- ChatBannerView -->
|
||||
<string name="chat_banner_connect_to_chat">Tap Connect to chat</string>
|
||||
<string name="chat_banner_send_request_to_connect">Tap Connect to send request</string>
|
||||
<string name="chat_banner_accept_contact_request">Accept contact request</string>
|
||||
<string name="chat_banner_your_contact">Your contact</string>
|
||||
<string name="chat_banner_join_group">Tap Join group</string>
|
||||
<string name="chat_banner_your_group">Your group</string>
|
||||
<string name="chat_banner_group">Group</string>
|
||||
<string name="chat_banner_business_connection">Business connection</string>
|
||||
<string name="chat_banner_your_business_contact">Your business contact</string>
|
||||
|
||||
<!-- ShareListView.kt -->
|
||||
<string name="share_message">Share message…</string>
|
||||
<string name="share_image">Share media…</string>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#1f1f1f"><path d="M515-451.5 460-396q-9 8.5-9 19.75t9 20.25q9 9 20.25 9t20.2-9L604-459.95q8.5-8.37 8.5-20 0-11.64-8.5-20.55l-104-104q-8.5-8.5-19.75-8.5T460-604.25q-9 8.75-9 20t9 20.25l55 55H360q-12.5 0-20.5 8.25t-8 20.75q0 12.5 8 20.5t20.5 8h155ZM480.06-85q-80.97 0-153.13-31.26-72.15-31.27-125.79-85Q147.5-255 116.25-327.02 85-399.05 85-479.94q0-81.97 31.26-154.13 31.27-72.15 85-125.54Q255-813 327.02-844q72.03-31 152.92-31 81.97 0 154.13 31.13 72.17 31.13 125.55 84.5Q813-706 844-633.98q31 72.03 31 153.92 0 80.97-31.01 153.13-31.02 72.15-84.5 125.79Q706-147.5 633.98-116.25 561.95-85 480.06-85Zm-.09-57.5q140.53 0 239.03-98.97 98.5-98.96 98.5-238.5 0-140.53-98.47-239.03-98.46-98.5-239-98.5-139.53 0-238.53 98.47-99 98.46-99 239 0 139.53 98.97 238.53 98.96 99 238.5 99ZM480-480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 885 B |
Reference in New Issue
Block a user