mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 00:39:00 +00:00
Merge branch 'stable'
This commit is contained in:
@@ -240,6 +240,7 @@ desktop/src/jvmMain/kotlin/chat/simplex/desktop/ -- Desktop app (1 file)
|
||||
| common/.../common/ui/theme/Theme.kt | spec/services/theme.md | product/views/settings.md |
|
||||
| common/.../common/ui/theme/Color.kt | spec/services/theme.md | product/views/settings.md |
|
||||
| common/.../common/views/chatlist/ChatListView.kt | spec/client/chat-list.md | product/views/chat-list.md |
|
||||
| common/.../common/views/chatlist/GetStakeBanner.kt | spec/client/chat-list.md | product/views/chat-list.md |
|
||||
| common/.../common/views/chatlist/ChatListNavLinkView.kt | spec/client/chat-list.md | product/views/chat-list.md |
|
||||
| common/.../common/views/chatlist/ChatPreviewView.kt | spec/client/chat-list.md | product/views/chat-list.md |
|
||||
| common/.../common/views/chatlist/UserPicker.kt | spec/client/chat-list.md | product/views/chat-list.md |
|
||||
|
||||
@@ -397,9 +397,11 @@ fun CenterPartOfScreen() {
|
||||
}
|
||||
when (currentChatId.value) {
|
||||
null -> {
|
||||
if (shouldShowOnboarding()) {
|
||||
if (rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) {
|
||||
ModalManager.center.showInView()
|
||||
} else if (shouldShowOnboarding()) {
|
||||
ConnectOnboardingView()
|
||||
} else if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) {
|
||||
} else {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -408,8 +410,6 @@ fun CenterPartOfScreen() {
|
||||
) {
|
||||
Text(stringResource(if (chatModel.desktopNoUserNoRemote) MR.strings.no_connected_mobile else MR.strings.no_selected_chat))
|
||||
}
|
||||
} else {
|
||||
ModalManager.center.showInView()
|
||||
}
|
||||
}
|
||||
else -> ChatView(chatsCtx = chatModel.chatsContext, currentChatId) {}
|
||||
|
||||
+6
@@ -193,6 +193,8 @@ class AppPreferences {
|
||||
val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false)
|
||||
val addressCreationCardShown = mkBoolPreference(SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN, false)
|
||||
val supporterBannerShown = mkBoolPreference(SHARED_PREFS_SUPPORTER_BANNER_SHOWN, false)
|
||||
val getStakeBannerTapped = mkBoolPreference(SHARED_PREFS_GET_STAKE_BANNER_TAPPED, false)
|
||||
val getStakeBannerDismissed = mkBoolPreference(SHARED_PREFS_GET_STAKE_BANNER_DISMISSED, false)
|
||||
val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true)
|
||||
val showReportsInSupportChatAlert = mkBoolPreference(SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT, true)
|
||||
val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null)
|
||||
@@ -275,6 +277,8 @@ class AppPreferences {
|
||||
hintPref(oneHandUICardShown, false),
|
||||
hintPref(addressCreationCardShown, false),
|
||||
hintPref(supporterBannerShown, false),
|
||||
hintPref(getStakeBannerTapped, false),
|
||||
hintPref(getStakeBannerDismissed, false),
|
||||
hintPref(liveMessageAlertShown, false),
|
||||
hintPref(signMessageAlertShown, false),
|
||||
hintPref(showHiddenProfilesNotice, true),
|
||||
@@ -467,6 +471,8 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown"
|
||||
private const val SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN = "AddressCreationCardShown"
|
||||
private const val SHARED_PREFS_SUPPORTER_BANNER_SHOWN = "SupporterBannerShown"
|
||||
private const val SHARED_PREFS_GET_STAKE_BANNER_TAPPED = "GetStakeBannerTapped"
|
||||
private const val SHARED_PREFS_GET_STAKE_BANNER_DISMISSED = "GetStakeBannerDismissed"
|
||||
private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert"
|
||||
private const val SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "ShowReportsInSupportChatAlert"
|
||||
private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase"
|
||||
|
||||
+14
@@ -950,6 +950,10 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state }
|
||||
val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state }
|
||||
val supporterBannerShown = remember { appPrefs.supporterBannerShown.state }
|
||||
val getStakeBannerTapped = remember { appPrefs.getStakeBannerTapped.state }
|
||||
val getStakeBannerDismissed = remember { appPrefs.getStakeBannerDismissed.state }
|
||||
// read here rather than in the LazyColumn: it launches an effect, so it needs a composable scope
|
||||
val crowdfunding = crowdfundingAvailable()
|
||||
val activeFilter = remember { chatModel.activeChatTagFilter }
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
@@ -1060,6 +1064,16 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (crowdfunding && !getStakeBannerDismissed.value) {
|
||||
item {
|
||||
Box(Modifier.zIndex(1f).padding(16.dp)) {
|
||||
GetStakeBanner(
|
||||
showDismiss = getStakeBannerTapped.value && chatModel.chats.value.isNotEmpty(),
|
||||
onTap = { openGetStake(ModalManager.start) },
|
||||
onDismiss = { appPrefs.getStakeBannerDismissed.set(true) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
itemsIndexed(chats, key = { _, chat -> chat.remoteHostId to chat.id }) { index, chat ->
|
||||
val nextChatSelected = remember(chat.id, chats) { derivedStateOf {
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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.model.ChatController.appPrefs
|
||||
import chat.simplex.common.ui.theme.isInDarkTheme
|
||||
import chat.simplex.common.views.helpers.ModalManager
|
||||
import chat.simplex.common.views.helpers.fontSizeMultiplier
|
||||
import chat.simplex.common.views.newchat.darkStops
|
||||
import chat.simplex.common.views.newchat.gradientPoints
|
||||
import chat.simplex.common.views.newchat.lightStops
|
||||
import chat.simplex.common.views.onboarding.GetStakeView
|
||||
import chat.simplex.res.MR
|
||||
|
||||
// Spec: spec/client/chat-list.md#GetStakeBanner
|
||||
@Composable
|
||||
fun GetStakeBanner(showDismiss: Boolean, onTap: () -> Unit, onDismiss: () -> Unit) {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
Modifier
|
||||
.bannerCard(onTap)
|
||||
// the end padding is the 8dp trailing inset plus the X's 36dp hit region
|
||||
.padding(start = 16.dp, end = 44.dp, top = 12.dp, bottom = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
stringResource(MR.strings.invest_banner_title),
|
||||
style = MaterialTheme.typography.body1,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colors.primary,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
stringResource(MR.strings.invest_banner_subtitle),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showDismiss) {
|
||||
BannerDismissButton(Modifier.align(Alignment.TopEnd), onDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openGetStake(modalManager: ModalManager) {
|
||||
appPrefs.getStakeBannerTapped.set(true)
|
||||
modalManager.showModalCloseable(cardScreen = true) { close ->
|
||||
GetStakeView(showFirstImage = true, inCenterOfWindow = modalManager === ModalManager.center, close = close)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.bannerCard(onTap: () -> Unit): Modifier {
|
||||
// grows linearly with system font but never shrinks below the default, and the card grows further
|
||||
// when 2-line text wraps at very large fonts
|
||||
val cardHeight = (72.dp * fontSizeMultiplier).coerceAtLeast(72.dp)
|
||||
val isDark = isInDarkTheme()
|
||||
var cardSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
val brush = remember(isDark, cardSize) { gradientBrush(isDark, cardSize) }
|
||||
return this
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cardHeight)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(brush)
|
||||
.clickable(onClick = onTap)
|
||||
.onSizeChanged { cardSize = it }
|
||||
}
|
||||
|
||||
// Same X pattern as OneHandUICard: circle-clipped clickable region with inner padding for hit area.
|
||||
@Composable
|
||||
fun BannerDismissButton(modifier: Modifier, onDismiss: () -> Unit) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close),
|
||||
contentDescription = stringResource(MR.strings.icon_descr_close_button),
|
||||
tint = if (isInDarkTheme()) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary,
|
||||
modifier = modifier
|
||||
.padding(end = 4.dp, top = 4.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable(onClick = onDismiss)
|
||||
.padding(8.dp)
|
||||
.size(16.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)
|
||||
)
|
||||
}
|
||||
+27
-2
@@ -23,6 +23,7 @@ import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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
|
||||
@@ -32,7 +33,10 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chatlist.GetStakeBanner
|
||||
import chat.simplex.common.views.chatlist.openGetStake
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.crowdfundingAvailable
|
||||
import chat.simplex.common.views.usersettings.UserAddressView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -415,6 +419,27 @@ fun ConnectOnboardingView() {
|
||||
}
|
||||
}
|
||||
|
||||
val getStakeBannerDismissed = remember { appPrefs.getStakeBannerDismissed.state }
|
||||
val showGetStakeBanner = 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 {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||
pager()
|
||||
}
|
||||
if (showGetStakeBanner) {
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).widthIn(max = bannerMaxWidth).padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = 8.dp)) {
|
||||
GetStakeBanner(
|
||||
showDismiss = false,
|
||||
onTap = cardClickOverride ?: { openGetStake(if (appPlatform.isDesktop) ModalManager.center else ModalManager.start) },
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (appPlatform.isDesktop) {
|
||||
val maxContentWidth = DEFAULT_WINDOW_WIDTH - DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier
|
||||
Box(
|
||||
@@ -422,12 +447,12 @@ fun ConnectOnboardingView() {
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(Modifier.widthIn(max = maxContentWidth).fillMaxHeight()) {
|
||||
pager()
|
||||
content()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
pager()
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1088,7 +1088,7 @@ fun isInUs(): Boolean =
|
||||
@Composable
|
||||
private fun InvestInSimpleXChatView(modalManager: ModalManager) {
|
||||
if (!crowdfundingAvailable()) return
|
||||
val showGetStake = { modalManager.showModalCloseable(cardScreen = true) { close -> GetStakeView(fromSettings = false, inCenterOfWindow = crowdfundingLayout.inCenterOfWindow(modalManager), close = close) } }
|
||||
val showGetStake = { modalManager.showModalCloseable(cardScreen = true) { close -> GetStakeView(showFirstImage = false, inCenterOfWindow = crowdfundingLayout.inCenterOfWindow(modalManager), close = close) } }
|
||||
Column(modifier = Modifier.padding(bottom = 12.dp)) {
|
||||
Text(
|
||||
generalGetString(MR.strings.v7_0_invest),
|
||||
@@ -1170,7 +1170,7 @@ private val getStakeSlides: List<CrowdfundingSlide> = listOf(
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun GetStakeView(fromSettings: Boolean, inCenterOfWindow: Boolean = false, close: () -> Unit) {
|
||||
fun GetStakeView(showFirstImage: Boolean, inCenterOfWindow: Boolean = false, close: () -> Unit) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
|
||||
@@ -1200,7 +1200,7 @@ fun GetStakeView(fromSettings: Boolean, inCenterOfWindow: Boolean = false, close
|
||||
val title = "Get a stake in\nSimpleX Chat"
|
||||
AppBarTitle(if (inCenterOfWindow) title.replace("\n", " ") else title, withPadding = false)
|
||||
// What's new already shows the image of the first slide, above the link that opens this page
|
||||
if (fromSettings) {
|
||||
if (showFirstImage) {
|
||||
slideImage(getStakeSlides[0])
|
||||
}
|
||||
Text(
|
||||
@@ -1213,7 +1213,7 @@ fun GetStakeView(fromSettings: Boolean, inCenterOfWindow: Boolean = false, close
|
||||
}
|
||||
}
|
||||
},
|
||||
Modifier.padding(top = if (fromSettings) 8.dp else 0.dp),
|
||||
Modifier.padding(top = if (showFirstImage) 8.dp else 0.dp),
|
||||
lineHeight = 24.sp
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ fun SettingsLayout(
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_redeem),
|
||||
stringResource(MR.strings.v7_0_crowdfunding),
|
||||
{ ModalManager.start.showModalCloseable(cardScreen = true) { close -> GetStakeView(fromSettings = true, close = close) } }
|
||||
{ ModalManager.start.showModalCloseable(cardScreen = true) { close -> GetStakeView(showFirstImage = true, close = close) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2678,6 +2678,8 @@
|
||||
<string name="v7_0_invest_descr" translatable="false">Crowdfunding on Wefunder.</string>
|
||||
<string name="v7_0_crowdfunding" translatable="false">Crowdfunding on Wefunder</string>
|
||||
<string name="v7_0_invest_learn_more" translatable="false">Learn more on Wefunder</string>
|
||||
<string name="invest_banner_title" translatable="false">Get a stake in SimpleX Chat!</string>
|
||||
<string name="invest_banner_subtitle" translatable="false">Invest on Wefunder from $100</string>
|
||||
<string name="v7_0_simplex_names">SimpleX public names (BETA)</string>
|
||||
<string name="v7_0_simplex_names_descr">Public names for your channel or business.</string>
|
||||
<string name="v7_0_channels">Better channels 📢</string>
|
||||
|
||||
@@ -19,7 +19,7 @@ This document provides a structured mapping between product-level concepts, thei
|
||||
|
||||
| # | Concept | Product Docs | Spec Docs | Source Files (Kotlin) | Source Files (Haskell) |
|
||||
|---|---------|-------------|-----------|----------------------|----------------------|
|
||||
| PC1 | Chat List | [README.md](README.md) (Navigation Map) | [spec/client/chat-list.md](../spec/client/chat-list.md) | `common/.../views/chatlist/ChatListView.kt`, `ChatListNavLinkView.kt`, `ChatPreviewView.kt` | `Controller.hs` (`APIGetChats`) |
|
||||
| PC1 | Chat List | [README.md](README.md) (Navigation Map) | [spec/client/chat-list.md](../spec/client/chat-list.md) | `common/.../views/chatlist/ChatListView.kt`, `ChatListNavLinkView.kt`, `ChatPreviewView.kt`, `GetStakeBanner.kt` | `Controller.hs` (`APIGetChats`) |
|
||||
| PC2 | Direct Chat | [README.md](README.md) (Messaging) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `common/.../views/chat/ChatView.kt`, `ChatInfoView.kt` | `Types.hs` (`Contact`), `Messages.hs` |
|
||||
| PC3 | Group Chat | [README.md](README.md) (Groups) | [spec/client/chat-view.md](../spec/client/chat-view.md) | `common/.../views/chat/ChatView.kt`, `group/GroupChatInfoView.kt` | `Types.hs` (`GroupInfo`, `GroupMember`) |
|
||||
| PC4 | Message Composition | [README.md](README.md) (Messaging) | [spec/client/compose.md](../spec/client/compose.md) | `common/.../views/chat/ComposeView.kt`, `SendMsgView.kt`, `ComposeVoiceView.kt`, `ComposeImageView.kt`, `ComposeFileView.kt` | `Controller.hs` (`APISendMessages`) |
|
||||
|
||||
@@ -115,6 +115,7 @@ Each chat type provides specific dropdown menu items:
|
||||
| One-hand UI card (`ToggleChatListCard`) | `oneHandUICardShown == false` | Dismissible card introducing bottom toolbar mode with toggle switch |
|
||||
| Address creation card (`AddressCreationCard`) | `addressCreationCardShown == false` | Prompts user to create a SimpleX address; tappable card opens `UserAddressLearnMore` |
|
||||
| FAB (new chat button) | Standard mode, search empty, chat running | `FloatingActionButton` at bottom-right, pencil icon, opens `NewChatSheet` |
|
||||
| Crowdfunding banner (`GetStakeBanner`) | `crowdfundingAvailable()` and not dismissed | Gradient card above the chats, and below the onboarding cards when there are no conversations; opens `GetStakeView`. The dismiss X appears once the banner has been tapped and there is at least one chat; dismissing hides it until hints are reset |
|
||||
|
||||
### Empty States
|
||||
|
||||
@@ -134,3 +135,4 @@ Each chat type provides specific dropdown menu items:
|
||||
| `ChatPreviewView.kt` | `views/chatlist/ChatPreviewView.kt` |
|
||||
| `UserPicker.kt` | `views/chatlist/UserPicker.kt` |
|
||||
| `TagListView.kt` | `views/chatlist/TagListView.kt` |
|
||||
| `GetStakeBanner.kt` | `views/chatlist/GetStakeBanner.kt` |
|
||||
|
||||
@@ -124,9 +124,9 @@ Common Module (commonMain)
|
||||
| Desktop Init | [`AppCommon.desktop.kt`](../common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt#L21) | `fun initApp()` | 21 |
|
||||
| Common App Screen | [`App.kt`](../common/src/commonMain/kotlin/chat/simplex/common/App.kt#L47) | `fun AppScreen()` | 47 |
|
||||
| JNI Bridge | [`Core.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt#L18) | `external fun initHS()` | 18 |
|
||||
| Chat Controller | [`SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L493) | `object ChatController` | 493 |
|
||||
| Chat Controller | [`SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L525) | `object ChatController` | 525 |
|
||||
| Chat Model | [`ChatModel.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L86) | `object ChatModel` | 86 |
|
||||
| App Preferences | [`SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L94) | `class AppPreferences` | 94 |
|
||||
| App Preferences | [`SimpleXAPI.kt`](../common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt#L102) | `class AppPreferences` | 102 |
|
||||
| Platform Interface | [`Platform.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt#L15) | `interface PlatformInterface` | 15 |
|
||||
| Notification Manager | [`NtfManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt#L19) | `abstract class NtfManager` | 19 |
|
||||
| Theme Manager | [`ThemeManager.kt`](../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt#L18) | `object ThemeManager` | 18 |
|
||||
|
||||
@@ -15,12 +15,13 @@ Source: `common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatLis
|
||||
7. [Tag System](#7-tag-system)
|
||||
8. [UserPicker](#8-userpicker)
|
||||
9. [Source Files](#9-source-files)
|
||||
10. [Crowdfunding Banner](#10-crowdfunding-banner)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The Chat List is the landing screen of SimpleX Chat, rendering all conversations for the active user. Built around `ChatListView` (line 126 in `ChatListView.kt`), it provides a searchable, filterable `LazyColumn` of chat previews with a toolbar, tag-based filtering, and a user-switching side panel. The view adapts between one-hand UI mode (toolbar at bottom, reversed list) and standard mode (toolbar at top). Search also accepts SimpleX links for direct connection.
|
||||
The Chat List is the landing screen of SimpleX Chat, rendering all conversations for the active user. Built around `ChatListView` (line 179 in `ChatListView.kt`), it provides a searchable, filterable `LazyColumn` of chat previews with a toolbar, tag-based filtering, and a user-switching side panel. The view adapts between one-hand UI mode (toolbar at bottom, reversed list) and standard mode (toolbar at top). Search also accepts SimpleX links for direct connection.
|
||||
|
||||
---
|
||||
|
||||
@@ -53,7 +54,7 @@ ChatListView
|
||||
|
||||
## 2. ChatListView Composable
|
||||
|
||||
**Location:** [`ChatListView.kt#L127`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L127)
|
||||
**Location:** [`ChatListView.kt#L179`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L179)
|
||||
|
||||
```kotlin
|
||||
fun ChatListView(
|
||||
@@ -66,8 +67,8 @@ fun ChatListView(
|
||||
|
||||
### Initialization
|
||||
|
||||
- Shows "What's New" modal on first launch after update (line ~130), with a 1-second delay.
|
||||
- On desktop, closing a chat resets audio/video players (line ~138).
|
||||
- Shows "What's New" modal on first launch after update (line ~185), with a 1-second delay.
|
||||
- On desktop, closing a chat resets audio/video players (line ~193).
|
||||
|
||||
### Layout Modes
|
||||
|
||||
@@ -88,8 +89,8 @@ The `oneHandUI` preference (`appPrefs.oneHandUI.state`) controls the layout:
|
||||
|
||||
### Android-specific
|
||||
|
||||
- `SetNotificationsModeAdditions`: Notification permission setup (line ~184).
|
||||
- `UserPicker`: Overlay side panel for user switching (line ~192).
|
||||
- `SetNotificationsModeAdditions`: Notification permission setup (line ~243).
|
||||
- `UserPicker`: Overlay side panel for user switching (line ~247).
|
||||
|
||||
---
|
||||
|
||||
@@ -113,7 +114,7 @@ The `oneHandUI` preference (`appPrefs.oneHandUI.state`) controls the layout:
|
||||
|
||||
### Active Filter Types
|
||||
|
||||
Defined as sealed class `ActiveFilter` (line ~51):
|
||||
Defined as sealed class `ActiveFilter` (line ~59):
|
||||
|
||||
```kotlin
|
||||
sealed class ActiveFilter {
|
||||
@@ -136,7 +137,7 @@ sealed class ActiveFilter {
|
||||
|
||||
### Search Filtering
|
||||
|
||||
The `filteredChats` function (line ~1188) applies filters in this order:
|
||||
The `filteredChats` function (line ~1474) applies filters in this order:
|
||||
|
||||
1. **SimpleX link match:** If a pasted link resolved to a known contact/group, show only that chat.
|
||||
2. **Text search:** Case-insensitive match against `chat.chatInfo.chatViewName`, `chat.chatInfo.fullName`, and `chat.chatInfo.localAlias`.
|
||||
@@ -147,7 +148,7 @@ The `filteredChats` function (line ~1188) applies filters in this order:
|
||||
|
||||
### Search Bar
|
||||
|
||||
`ChatListSearchBar` (line ~611) provides:
|
||||
`ChatListSearchBar` (line ~765) provides:
|
||||
- Text input with search icon.
|
||||
- SimpleX link detection: When a pasted string contains a single SimpleX link, it triggers `planAndConnect` for connection, suppressing normal search.
|
||||
- Unread filter toggle button (right side, when search is empty).
|
||||
@@ -158,7 +159,7 @@ The `filteredChats` function (line ~1188) applies filters in this order:
|
||||
|
||||
## 5. Chat Preview
|
||||
|
||||
**Location:** [`ChatPreviewView.kt#L40`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt#L40)
|
||||
**Location:** [`ChatPreviewView.kt#L41`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt#L41)
|
||||
|
||||
```kotlin
|
||||
fun ChatPreviewView(
|
||||
@@ -224,7 +225,7 @@ On desktop, the currently selected chat (`chatModel.chatId.value == chat.id`) re
|
||||
|
||||
### TagsView
|
||||
|
||||
**Location:** [`ChatListView.kt#L929`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L929)
|
||||
**Location:** [`ChatListView.kt#L1214`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt#L1214)
|
||||
|
||||
Renders a horizontally scrollable row of tag chips (via `TagsRow`, which is a platform-specific `expect` composable).
|
||||
|
||||
@@ -244,7 +245,7 @@ Layout logic:
|
||||
|
||||
### TagListView
|
||||
|
||||
**Location:** [`TagListView.kt#L48`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt#L48)
|
||||
**Location:** [`TagListView.kt#L47`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt#L47)
|
||||
|
||||
Full-screen tag management view opened from the "+" button or long-press menu.
|
||||
|
||||
@@ -312,3 +313,42 @@ Uses `AnimatedViewState` (`GONE`, `VISIBLE`, `HIDING`) with a `MutableStateFlow`
|
||||
| `ShareListView.kt` | Share target list (forwarding flow) |
|
||||
| `TagListView.kt` | Tag management and assignment view |
|
||||
| `UserPicker.kt` | User switching side panel |
|
||||
| `GetStakeBanner.kt` | Crowdfunding banner and the shared banner card chrome |
|
||||
|
||||
---
|
||||
|
||||
<a id="GetStakeBanner"></a>
|
||||
|
||||
## 10. Crowdfunding Banner
|
||||
|
||||
**Location:** [`GetStakeBanner.kt#L31`](../../common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/GetStakeBanner.kt#L31)
|
||||
|
||||
```kotlin
|
||||
fun GetStakeBanner(showDismiss: Boolean, onTap: () -> Unit, onDismiss: () -> Unit)
|
||||
```
|
||||
|
||||
Gradient card inviting the user to invest on Wefunder. Shown only when `crowdfundingAvailable()` — always outside Play Store builds, and in Play builds only while the store country is the US or not yet known — and only while `getStakeBannerDismissed` is false.
|
||||
|
||||
### Placement
|
||||
|
||||
| Where | Condition | Layout |
|
||||
|-------|-----------|--------|
|
||||
| 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)` |
|
||||
|
||||
`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
|
||||
|
||||
| Preference | Set by | Effect |
|
||||
|------------|--------|--------|
|
||||
| `getStakeBannerTapped` | `openGetStake()` | the dismiss X appears from then on, while there are chats |
|
||||
| `getStakeBannerDismissed` | the dismiss X | hides the banner in both placements |
|
||||
|
||||
Both are in `AppPreferences.hintPreferences`, so "Reset all hints" restores the banner. The X is never offered below the onboarding cards, so the banner cannot be dismissed before the user has a chat.
|
||||
|
||||
Tapping the card runs `openGetStake()`, which opens `GetStakeView(showFirstImage = true)` as a card modal — `showFirstImage` decides whether the page repeats the first slide's image, which only What's New shows above its own link.
|
||||
|
||||
### Shared card chrome
|
||||
|
||||
`Modifier.bannerCard(onTap)` (size state, gradient brush, `heightIn`, `clip`, `background`, `clickable`) and `BannerDismissButton` are declared separately from `GetStakeBanner` so other banners can adopt the same chrome; the paddings stay with the caller. The gradient reuses `gradientPoints`, `lightStops` and `darkStops` from `views/newchat/OnboardingCards.kt`.
|
||||
|
||||
@@ -92,6 +92,7 @@ Path prefix: `common/src/commonMain/kotlin/chat/simplex/common/`
|
||||
| Source File | Product Concepts Affected | Risk Level | Notes |
|
||||
|-------------|--------------------------|------------|-------|
|
||||
| `views/chatlist/ChatListView.kt` | PC1, PC28 | High | Main screen — chat list rendering and search |
|
||||
| `views/chatlist/GetStakeBanner.kt` | PC1 | Low | Crowdfunding banner and shared banner card chrome |
|
||||
| `views/chatlist/ChatListNavLinkView.kt` | PC1, PC2, PC3 | Medium | Navigation from chat list item to chat |
|
||||
| `views/chatlist/ChatPreviewView.kt` | PC1, PC2, PC3, PC11 | Medium | Chat row preview rendering |
|
||||
| `views/chatlist/TagListView.kt` | PC28 | Medium | Chat tag filter UI |
|
||||
|
||||
Reference in New Issue
Block a user