mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-26 02:24:54 +00:00
ui: one banner at a time in chat list (#7554)
This commit is contained in:
@@ -383,6 +383,12 @@ class BadgeModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatListBanner {
|
||||
case badgeExpired
|
||||
case badgePitch
|
||||
case getStake
|
||||
}
|
||||
|
||||
// Spec: spec/state.md#ChatModel
|
||||
final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
@@ -463,6 +469,14 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
var filesToDelete: Set<URL> = []
|
||||
|
||||
// the banner kind the chat list showed this app session: it keeps the slot until restart, so dismissing it never puts
|
||||
// another in its place; only the badge alert shows regardless. Set while rendering, so not published.
|
||||
var chatListBanner: ChatListBanner?
|
||||
|
||||
func bannerSlotFree(for banner: ChatListBanner) -> Bool {
|
||||
chatListBanner == nil || chatListBanner == banner
|
||||
}
|
||||
|
||||
static let shared = ChatModel()
|
||||
|
||||
let im = ItemsModel.shared
|
||||
|
||||
@@ -17,14 +17,10 @@ struct SupportSimpleXBanner: View {
|
||||
let onTap: () -> Void
|
||||
let onDismiss: () -> Void
|
||||
|
||||
private let cardCornerRadius: CGFloat = 16
|
||||
// grows with Dynamic Type but never shrinks below the default so small-font users see the same
|
||||
// banner as today; hero stays fixed so its above-card overhang shrinks at very large fonts
|
||||
// the card's own height, for centring the fallback hero; hero stays fixed so its above-card
|
||||
// overhang shrinks at very large fonts
|
||||
@ScaledMetric(relativeTo: .body) private var scaledCardHeight: CGFloat = 72
|
||||
private var cardHeight: CGFloat { max(72, scaledCardHeight) }
|
||||
// matches OneHandUICard's segment icon leading so the text aligns with it in the list
|
||||
private let cardLeadingPadding: CGFloat = 16
|
||||
private let cardTrailingPadding: CGFloat = 8
|
||||
private let heroWidth: CGFloat = 110
|
||||
// shorter than the natural drawn height so .clipped() slices the phone body at card bottom
|
||||
private let heroVisibleHeight: CGFloat = 108
|
||||
@@ -50,12 +46,7 @@ struct SupportSimpleXBanner: View {
|
||||
}
|
||||
Spacer(minLength: heroWidth + heroTrailingPadding + textToHeroGap)
|
||||
}
|
||||
.padding(.leading, cardLeadingPadding)
|
||||
.padding(.trailing, cardTrailingPadding)
|
||||
.padding(.vertical, 12)
|
||||
.frame(minHeight: cardHeight)
|
||||
.background(gradientBackground())
|
||||
.clipShape(RoundedRectangle(cornerRadius: cardCornerRadius))
|
||||
.modifier(BannerCard())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
@@ -64,15 +55,7 @@ struct SupportSimpleXBanner: View {
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
Image(systemName: "multiply")
|
||||
.foregroundColor(colorScheme == .dark ? theme.colors.onBackground : theme.colors.secondary)
|
||||
.frame(width: 12, height: 12)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 4)
|
||||
.padding(.trailing, 16)
|
||||
.padding(.leading, 4)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture(perform: onDismiss)
|
||||
BannerDismissButton(onDismiss: onDismiss)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,25 +77,6 @@ struct SupportSimpleXBanner: View {
|
||||
.padding(.trailing, 12)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func gradientBackground() -> some View {
|
||||
// Asymmetric scale: start (dark end) pushed further below the card than the end (warm) is
|
||||
// above, so the card's middle lands at the bright/mid-transition stop instead of the dark
|
||||
// navy region. Keeps the small warm accent at top-right.
|
||||
GeometryReader { geo in
|
||||
let aspect = max(geo.size.height, 1) / max(geo.size.width, 1)
|
||||
let startScale: CGFloat = colorScheme == .light ? 2.5 : 3.0
|
||||
let endScale: CGFloat = colorScheme == .light ? 1.7 : 2.1
|
||||
let gp = OnboardingCardView.gradientPoints(aspectRatio: aspect, scale: 1.0)
|
||||
let start = UnitPoint(x: 0.5 + (gp.start.x - 0.5) * startScale, y: 0.5 + (gp.start.y - 0.5) * startScale)
|
||||
let end = UnitPoint(x: 0.5 + (gp.end.x - 0.5) * endScale, y: 0.5 + (gp.end.y - 0.5) * endScale)
|
||||
return LinearGradient(
|
||||
stops: colorScheme == .light ? OnboardingCardView.lightStops : OnboardingCardView.darkStops,
|
||||
startPoint: start,
|
||||
endPoint: end
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SupportSimpleXBanner_Previews: PreviewProvider {
|
||||
|
||||
@@ -388,8 +388,10 @@ struct ChatListView: View {
|
||||
badgeModel.alert?.kind == .supportEnded && badgeModel.userId == chatModel.currentUser?.userId
|
||||
}
|
||||
|
||||
private var hasShownBadge: Bool {
|
||||
badgeModel.badgeState?.shown == true && badgeModel.userId == chatModel.currentUser?.userId
|
||||
// false until the badge state loads: if the pitch rendered before that, it would lock the slot, and a supporter's badge
|
||||
// arriving a moment later would hide it, leaving the slot empty for the session
|
||||
private var noShownBadge: Bool {
|
||||
badgeModel.badgeState?.shown != true && badgeModel.userId == chatModel.currentUser?.userId
|
||||
}
|
||||
|
||||
private func showSupportEndedDismissAlert() {
|
||||
@@ -433,10 +435,11 @@ struct ChatListView: View {
|
||||
if shouldShowOnboarding {
|
||||
VStack(spacing: 0) {
|
||||
ConnectOnboardingView()
|
||||
if isInUS && !getStakeBannerDismissed {
|
||||
if chatModel.bannerSlotFree(for: .getStake) && isInUS && !getStakeBannerDismissed {
|
||||
GetStakeBanner(showDismiss: false, onTap: openGetStake, onDismiss: {})
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 8)
|
||||
.onAppear { chatModel.chatListBanner = .getStake }
|
||||
}
|
||||
}
|
||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||
@@ -492,7 +495,8 @@ struct ChatListView: View {
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
.zIndex(1)
|
||||
} else if !supporterBannerShown && !hasShownBadge && chatModel.chats.count > 3 {
|
||||
.onAppear { chatModel.chatListBanner = .badgeExpired }
|
||||
} else if chatModel.bannerSlotFree(for: .badgePitch) && !supporterBannerShown && noShownBadge && chatModel.chats.count > 3 {
|
||||
SupportSimpleXBanner(
|
||||
onTap: { showBadgesSheet = true },
|
||||
onDismiss: showSupportSimpleXDismissAlert
|
||||
@@ -502,7 +506,8 @@ struct ChatListView: View {
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
.zIndex(1)
|
||||
} else if isInUS && !getStakeBannerDismissed {
|
||||
.onAppear { chatModel.chatListBanner = .badgePitch }
|
||||
} else if chatModel.bannerSlotFree(for: .getStake) && isInUS && !getStakeBannerDismissed {
|
||||
GetStakeBanner(
|
||||
showDismiss: getStakeBannerTapped && !chatModel.chats.isEmpty,
|
||||
onTap: openGetStake,
|
||||
@@ -513,6 +518,7 @@ struct ChatListView: View {
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
.zIndex(1)
|
||||
.onAppear { chatModel.chatListBanner = .getStake }
|
||||
}
|
||||
if #available(iOS 16.0, *) {
|
||||
ForEach(cs, id: \.viewId) { chat in
|
||||
|
||||
@@ -293,6 +293,8 @@ Gradient card inviting the user to invest on Wefunder. Shown only when [`isInUS`
|
||||
| Chat list | rendered whenever [`chatListContent`](../../Shared/Views/ChatList/ChatListView.swift#L413) is, in the `List` after `OneHandUICard` and before the chats | `.padding(.vertical, 3)`, flipped for one-hand UI, `.zIndex(1)` |
|
||||
| Onboarding | below [`ConnectOnboardingView`](../../Shared/Views/NewChat/OnboardingCards.swift#L135) when `shouldShowOnboarding` | `.padding(.horizontal, 20)` (the onboarding cards' margin), `.padding(.bottom, 8)` |
|
||||
|
||||
The list has a single banner slot, filled by an `if`/`else if` chain in priority order: the support-ended alert (`supportEnded`), the pitch, then the Wefunder banner. Each banner records itself in `ChatModel.chatListBanner` (`.badgeExpired`, `.badgePitch`, `.getStake`) in its `onAppear`, and the pitch and Wefunder conditions start with `chatModel.bannerSlotFree(for:)` — true only while nothing else was shown this app session — so dismissing a banner never puts another in its place until restart. The alert has no such check: it takes the slot whenever present, and once shown it holds it. The pitch also requires `noShownBadge`, false until `BadgeModel` holds the current user's state, so it cannot take the slot from a supporter whose badge loads a moment later. The onboarding placement applies the same `bannerSlotFree` check and records `.getStake`.
|
||||
|
||||
In the onboarding branch the `.scaleEffect` and `ThemedBackground` are applied to the enclosing `VStack` rather than to each child, so the banner stays below the pages in both toolbar modes.
|
||||
|
||||
### Dismissal
|
||||
|
||||
@@ -163,6 +163,7 @@ ChatTagsModel (singleton -- filter state)
|
||||
|----------|------|-------------|------|
|
||||
| `messageDelivery` | `[Int64: () -> Void]` | Pending delivery confirmation callbacks | [L426](../Shared/Model/ChatModel.swift#L426) |
|
||||
| `filesToDelete` | `Set<URL>` | Files queued for deletion | [L428](../Shared/Model/ChatModel.swift#L428) |
|
||||
| `chatListBanner` | `ChatListBanner?` | The banner kind the chat list showed this app session; `bannerSlotFree(for:)` tells whether a kind may take the slot (see [chat-list.md](client/chat-list.md)) | [L474](../Shared/Model/ChatModel.swift#L474) |
|
||||
| `im` | `ItemsModel` | Reference to `ItemsModel.shared` | [L432](../Shared/Model/ChatModel.swift#L432) |
|
||||
|
||||
### Key Methods
|
||||
|
||||
@@ -130,6 +130,8 @@ object BadgeModel {
|
||||
this.rhId.value == rhId && this.userId.value == userId
|
||||
}
|
||||
|
||||
enum class ChatListBanner { BadgeExpired, BadgePitch, GetStake }
|
||||
|
||||
/*
|
||||
* Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it
|
||||
* */
|
||||
@@ -199,6 +201,12 @@ object ChatModel {
|
||||
// Needed to apply black color to left/right cutout area on Android
|
||||
val fullscreenGalleryVisible = mutableStateOf(false)
|
||||
|
||||
// the banner kind the chat list showed this app session: it keeps the slot until restart, so dismissing it never puts
|
||||
// another in its place; only the badge alert shows regardless. Set while rendering, so not a state.
|
||||
var chatListBanner: ChatListBanner? = null
|
||||
|
||||
fun bannerSlotFree(banner: ChatListBanner): Boolean = chatListBanner == null || chatListBanner == banner
|
||||
|
||||
// preferences
|
||||
val notificationPreviewMode by lazy {
|
||||
mutableStateOf(
|
||||
|
||||
+7
-62
@@ -2,32 +2,23 @@ package chat.simplex.common.views.badges
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.BannerDismissButton
|
||||
import chat.simplex.common.views.chatlist.bannerCard
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.darkStops
|
||||
import chat.simplex.common.views.newchat.gradientPoints
|
||||
import chat.simplex.common.views.newchat.lightStops
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
@@ -37,13 +28,9 @@ fun SupportSimpleXBanner(
|
||||
onTap: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val cardCornerRadius = 16.dp
|
||||
// grows linearly with system font but never shrinks below the default so small-font users see the
|
||||
// same baseline; the card Row uses heightIn(min = cardHeight) and grows further when 2-line text
|
||||
// wraps at very large fonts. Hero stays fixed so its above-card overhang shrinks at very large fonts.
|
||||
// the card's own height, for centring the fallback hero; hero stays fixed so its above-card
|
||||
// overhang shrinks at very large fonts
|
||||
val cardHeight = (72.dp * fontSizeMultiplier).coerceAtLeast(72.dp)
|
||||
// matches OneHandUICard's segment icon leading so the text aligns with it in the list
|
||||
val cardLeadingPadding = 16.dp
|
||||
val cardTrailingPadding = 8.dp
|
||||
val heroWidth = 110.dp
|
||||
// shorter than the natural drawn height so ContentScale.Crop slices the phone body at card bottom
|
||||
@@ -52,24 +39,15 @@ fun SupportSimpleXBanner(
|
||||
val heroTrailingPadding = 28.dp
|
||||
val textToHeroGap = 6.dp
|
||||
|
||||
val isDark = isInDarkTheme()
|
||||
var cardSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
val brush = remember(isDark, cardSize) { gradientBrush(isDark, cardSize) }
|
||||
|
||||
// Layout sizes to the card; hero is placed at y = cardHeight - heroHeight (negative → hero
|
||||
// overhangs above card at normal fonts, 0/positive → hero fits inside card at large fonts).
|
||||
Layout(content = {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cardHeight)
|
||||
.clip(RoundedCornerShape(cardCornerRadius))
|
||||
.background(brush)
|
||||
.clickable(onClick = onTap)
|
||||
.onSizeChanged { cardSize = it }
|
||||
.bannerCard(onTap)
|
||||
.padding(
|
||||
start = cardLeadingPadding,
|
||||
start = 16.dp,
|
||||
end = cardTrailingPadding + heroWidth + heroTrailingPadding + textToHeroGap,
|
||||
top = 12.dp,
|
||||
bottom = 12.dp
|
||||
@@ -95,19 +73,7 @@ fun SupportSimpleXBanner(
|
||||
}
|
||||
}
|
||||
|
||||
// Same X pattern as OneHandUICard: circle-clipped clickable region with inner padding for hit area.
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close),
|
||||
contentDescription = stringResource(MR.strings.icon_descr_close_button),
|
||||
tint = if (isDark) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(end = 4.dp, top = 4.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable(onClick = onDismiss)
|
||||
.padding(8.dp)
|
||||
.size(16.dp)
|
||||
)
|
||||
BannerDismissButton(Modifier.align(Alignment.TopEnd), onDismiss)
|
||||
}
|
||||
|
||||
HeroThumbnail(
|
||||
@@ -149,24 +115,3 @@ private fun HeroThumbnail(heroWidth: Dp, heroVisibleHeight: Dp, cardHeight: Dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Geometry-aware gradient with asymmetric scale: start (dark) pushed further below the card than
|
||||
// end (warm) is above, so card-middle lands at the bright/mid-transition stop, not the dark region.
|
||||
private fun gradientBrush(isDark: Boolean, size: IntSize): Brush {
|
||||
val stops = if (isDark) darkStops else lightStops
|
||||
if (size.width == 0 || size.height == 0) return Brush.linearGradient(colorStops = stops)
|
||||
val w = size.width.toFloat()
|
||||
val h = size.height.toFloat()
|
||||
val startScale = if (isDark) 3.0f else 2.5f
|
||||
val endScale = if (isDark) 2.1f else 1.7f
|
||||
val gp = gradientPoints(h / w, 1.0f)
|
||||
val sx = 0.5f + (gp.startX - 0.5f) * startScale
|
||||
val sy = 0.5f + (gp.startY - 0.5f) * startScale
|
||||
val ex = 0.5f + (gp.endX - 0.5f) * endScale
|
||||
val ey = 0.5f + (gp.endY - 0.5f) * endScale
|
||||
return Brush.linearGradient(
|
||||
colorStops = stops,
|
||||
start = Offset(sx * w, sy * h),
|
||||
end = Offset(ex * w, ey * h)
|
||||
)
|
||||
}
|
||||
|
||||
+5
-2
@@ -1046,6 +1046,7 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
val alert = BadgeModel.alert.value
|
||||
if (supportEnded() && alert != null) {
|
||||
item {
|
||||
SideEffect { chatModel.chatListBanner = ChatListBanner.BadgeExpired }
|
||||
Box(Modifier.zIndex(1f).padding(16.dp)) {
|
||||
SupportSimpleXBanner(
|
||||
title = stringResource(MR.strings.badges_support_ended),
|
||||
@@ -1055,8 +1056,9 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (!supporterBannerShown.value && !hasShownBadge() && chatModel.chats.value.size > 3) {
|
||||
} else if (chatModel.bannerSlotFree(ChatListBanner.BadgePitch) && !supporterBannerShown.value && noShownBadge() && chatModel.chats.value.size > 3) {
|
||||
item {
|
||||
SideEffect { chatModel.chatListBanner = ChatListBanner.BadgePitch }
|
||||
Box(Modifier.zIndex(1f).padding(16.dp)) {
|
||||
SupportSimpleXBanner(
|
||||
onTap = { ModalManager.start.showCustomModal { close -> BadgesView(close) } },
|
||||
@@ -1064,8 +1066,9 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (crowdfunding && !getStakeBannerDismissed.value) {
|
||||
} else if (chatModel.bannerSlotFree(ChatListBanner.GetStake) && crowdfunding && !getStakeBannerDismissed.value) {
|
||||
item {
|
||||
SideEffect { chatModel.chatListBanner = ChatListBanner.GetStake }
|
||||
Box(Modifier.zIndex(1f).padding(16.dp)) {
|
||||
GetStakeBanner(
|
||||
showDismiss = getStakeBannerTapped.value && chatModel.chats.value.isNotEmpty(),
|
||||
|
||||
+6
-3
@@ -58,8 +58,10 @@ fun shouldShowOnboarding(): Boolean {
|
||||
fun supportEnded(): Boolean =
|
||||
BadgeModel.alert.value?.kind == BadgeAlertKind.SupportEnded && BadgeModel.isCurrent(chatModel.remoteHostId(), chatModel.currentUser.value?.userId)
|
||||
|
||||
fun hasShownBadge(): Boolean =
|
||||
BadgeModel.badgeState.value?.shown == true && BadgeModel.isCurrent(chatModel.remoteHostId(), chatModel.currentUser.value?.userId)
|
||||
// false until the badge state loads: if the pitch rendered before that, it would lock the slot, and a supporter's badge
|
||||
// arriving a moment later would hide it, leaving the slot empty for the session
|
||||
fun noShownBadge(): Boolean =
|
||||
BadgeModel.badgeState.value?.shown != true && BadgeModel.isCurrent(chatModel.remoteHostId(), chatModel.currentUser.value?.userId)
|
||||
|
||||
fun hasConversations(chats: List<Chat>): Boolean =
|
||||
chats.any { chat ->
|
||||
@@ -420,7 +422,7 @@ fun ConnectOnboardingView() {
|
||||
}
|
||||
|
||||
val getStakeBannerDismissed = remember { appPrefs.getStakeBannerDismissed.state }
|
||||
val showGetStakeBanner = crowdfundingAvailable() && !getStakeBannerDismissed.value
|
||||
val showGetStakeBanner = chatModel.bannerSlotFree(ChatListBanner.GetStake) && crowdfundingAvailable() && !getStakeBannerDismissed.value
|
||||
// on desktop the pages span the window, but the banner keeps the width it has in the chat list
|
||||
val bannerMaxWidth = if (appPlatform.isDesktop) DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier else Dp.Unspecified
|
||||
val content = @Composable {
|
||||
@@ -429,6 +431,7 @@ fun ConnectOnboardingView() {
|
||||
pager()
|
||||
}
|
||||
if (showGetStakeBanner) {
|
||||
SideEffect { chatModel.chatListBanner = ChatListBanner.GetStake }
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).widthIn(max = bannerMaxWidth).padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = 8.dp)) {
|
||||
GetStakeBanner(
|
||||
showDismiss = false,
|
||||
|
||||
@@ -336,6 +336,8 @@ Gradient card inviting the user to invest on Wefunder. Shown only when `crowdfun
|
||||
| Chat list | in `ChatList`'s `LazyColumn`, after `ToggleChatListCard` and before the chats | `Box(Modifier.zIndex(1f).padding(16.dp))` |
|
||||
| Onboarding | inside `ConnectOnboardingView` (`views/newchat/OnboardingCards.kt`), below the pager, so it shares the pages' width limit on desktop and their dimming while a start modal is open; opens the page in `ModalManager.center` on desktop, `ModalManager.start` on Android | `padding(start/end = DEFAULT_PADDING, bottom = 8.dp)`, in a `Column` where the pager takes `weight(1f)` |
|
||||
|
||||
The list has a single banner slot, filled by an `if`/`else if` chain in priority order: the support-ended alert (`supportEnded()`), the pitch, then the Wefunder banner. Each banner's `item` records itself in `ChatModel.chatListBanner` (`BadgeExpired`, `BadgePitch`, `GetStake`) in a `SideEffect`, and the pitch and Wefunder conditions start with `chatModel.bannerSlotFree(banner)` — true only while nothing else was shown this app session — so dismissing a banner never puts another in its place until restart. The alert has no such check: it takes the slot whenever present, and once shown it holds it. The pitch also requires `noShownBadge()` (`views/newchat/OnboardingCards.kt`), false until `BadgeModel.isCurrent` for the current user, so it cannot take the slot from a supporter whose badge loads a moment later. `ConnectOnboardingView` applies the same `bannerSlotFree` check and records `GetStake`.
|
||||
|
||||
`crowdfundingAvailable()` launches an effect to load the store country, so both call sites read it in the composable body rather than inside the `LazyColumn` builder.
|
||||
|
||||
### Dismissal
|
||||
|
||||
@@ -152,6 +152,7 @@ Defined at [`ChatModel.kt line 86`](../common/src/commonMain/kotlin/chat/simplex
|
||||
| [`appOpenUrlConnecting`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L138) | `MutableState<Boolean>` | 138 | Whether a deep link connection is in progress |
|
||||
| [`newChatSheetVisible`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L141) | `MutableState<Boolean>` | 141 | Whether new chat bottom sheet is visible |
|
||||
| [`fullscreenGalleryVisible`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L144) | `MutableState<Boolean>` | 144 | Fullscreen gallery mode |
|
||||
| [`chatListBanner`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L206) | `ChatListBanner?` (plain var) | 206 | The banner kind the chat list showed this app session; `bannerSlotFree(banner)` tells whether a kind may take the slot (see [chat-list.md](client/chat-list.md)) |
|
||||
| [`notificationPreviewMode`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L147) | `MutableState<NotificationPreviewMode>` | 147 | Notification content preview level |
|
||||
| [`showAuthScreen`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L156) | `MutableState<Boolean>` | 156 | Whether to show authentication screen |
|
||||
| [`showChatPreviews`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L158) | `MutableState<Boolean>` | 158 | Whether to show chat preview text in list |
|
||||
|
||||
Reference in New Issue
Block a user