mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 17:13:10 +00:00
Merge branch 'stable' into stable-android
This commit is contained in:
43 files changed
+632
-89
No files matched your search
@@ -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 |
|
||||
|
||||
@@ -100,12 +100,18 @@
|
||||
<data android:host="smp4.simplexonflux.com" />
|
||||
<data android:host="smp5.simplexonflux.com" />
|
||||
<data android:host="smp6.simplexonflux.com" />
|
||||
<data android:pathPrefix="/invitation" />
|
||||
<data android:pathPrefix="/contact" />
|
||||
<data android:pathPrefix="/a" />
|
||||
<data android:pathPrefix="/c" />
|
||||
<data android:pathPrefix="/g" />
|
||||
<data android:pathPrefix="/i" />
|
||||
<data android:path="/invitation" />
|
||||
<data android:path="/contact" />
|
||||
<data android:path="/a" />
|
||||
<data android:path="/c" />
|
||||
<data android:path="/g" />
|
||||
<data android:path="/i" />
|
||||
<data android:pathPrefix="/invitation/" />
|
||||
<data android:pathPrefix="/contact/" />
|
||||
<data android:pathPrefix="/a/" />
|
||||
<data android:pathPrefix="/c/" />
|
||||
<data android:pathPrefix="/g/" />
|
||||
<data android:pathPrefix="/i/" />
|
||||
</intent-filter>
|
||||
<!-- Receive files from other apps -->
|
||||
<intent-filter>
|
||||
|
||||
@@ -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
@@ -192,6 +192,8 @@ class AppPreferences {
|
||||
val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true)
|
||||
val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false)
|
||||
val addressCreationCardShown = mkBoolPreference(SHARED_PREFS_ADDRESS_CREATION_CARD_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)
|
||||
@@ -273,6 +275,8 @@ class AppPreferences {
|
||||
hintPref(laNoticeShown, false),
|
||||
hintPref(oneHandUICardShown, false),
|
||||
hintPref(addressCreationCardShown, false),
|
||||
hintPref(getStakeBannerTapped, false),
|
||||
hintPref(getStakeBannerDismissed, false),
|
||||
hintPref(liveMessageAlertShown, false),
|
||||
hintPref(signMessageAlertShown, false),
|
||||
hintPref(showHiddenProfilesNotice, true),
|
||||
@@ -464,6 +468,8 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE = "ShowHiddenProfilesNotice"
|
||||
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_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"
|
||||
|
||||
+1
-1
@@ -622,7 +622,7 @@ fun stripFormattedTextLink(ft: List<FormattedText>?, link: String): List<Formatt
|
||||
val i = result.lastIndex
|
||||
if (i >= 0 && result[i].format == null && result[i].text.endsWith("\n")) {
|
||||
result[i] = FormattedText(result[i].text.dropLast(1), null)
|
||||
if (result[i].text.isEmpty()) result.removeLast()
|
||||
if (result[i].text.isEmpty()) result.removeAt(result.lastIndex)
|
||||
}
|
||||
return result.ifEmpty { null }
|
||||
}
|
||||
+16
-1
@@ -742,7 +742,7 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) {
|
||||
} else {
|
||||
withBGApi {
|
||||
chatModel.appOpenUrlConnecting.value = true
|
||||
planAndConnect(rhId, uri, close = null, cleanup = { chatModel.appOpenUrlConnecting.value = false })
|
||||
planAndConnect(rhId, uri, close = { ModalManager.closeAllModalsEverywhere() }, cleanup = { chatModel.appOpenUrlConnecting.value = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -912,6 +912,10 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state }
|
||||
val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.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) {
|
||||
@@ -1000,6 +1004,17 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||
ToggleChatListCard()
|
||||
}
|
||||
}
|
||||
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 {
|
||||
chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
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)
|
||||
.padding(start = 16.dp, end = if (showDismiss) 40.dp else 18.dp, top = 12.dp, bottom = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Column(Modifier.weight(1f), 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
|
||||
)
|
||||
}
|
||||
Image(
|
||||
painterResource(if (isInDarkTheme()) MR.images.decentralized_light else MR.images.decentralized),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(37.dp)
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -407,6 +411,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(
|
||||
@@ -414,12 +439,12 @@ fun ConnectOnboardingView() {
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(Modifier.widthIn(max = maxContentWidth).fillMaxHeight()) {
|
||||
pager()
|
||||
content()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
pager()
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -985,7 +985,7 @@ fun shouldShowWhatsNew(m: ChatModel): Boolean {
|
||||
return v != lastVersion
|
||||
}
|
||||
|
||||
private const val WEFUNDER_URL = "https://wefunder.com/simplex.chat"
|
||||
private const val WEFUNDER_URL = "https://wefunder.com/simplex.chat?utm_source=app"
|
||||
|
||||
private const val CROWDFUNDING_CONTACT_URI = "simplex:/a#JxGcOA1_QhlmVFzYYabloMbvMZk5Y9d9iS3ITDnhzYo?h=smp11.simplex.im"
|
||||
|
||||
@@ -1040,7 +1040,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),
|
||||
@@ -1122,7 +1122,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
|
||||
|
||||
@@ -1149,10 +1149,10 @@ fun GetStakeView(fromSettings: Boolean, inCenterOfWindow: Boolean = false, close
|
||||
|
||||
ColumnWithScrollBar(Modifier.pinchZoom().padding(horizontal = DEFAULT_PADDING)) {
|
||||
// in the center of the window the page is wide enough for the title to fit on one line
|
||||
val title = "Get a stake in\nSimpleX Chat"
|
||||
val title = "Invest 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(
|
||||
@@ -1165,7 +1165,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
|
||||
)
|
||||
|
||||
|
||||
+5
-1
@@ -73,6 +73,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: (
|
||||
val simplexTeamUri =
|
||||
"simplex:/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw?h=smp6.simplex.im"
|
||||
|
||||
val simplexNewsUri =
|
||||
"simplex:/c#grcfG3ulVI4Sh6ow33qBsmSk7uEy3gRSl2KkJ5ER6tA?h=smp18.simplex.im"
|
||||
|
||||
@Composable
|
||||
fun SettingsLayout(
|
||||
stopped: Boolean,
|
||||
@@ -119,7 +122,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) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -148,6 +151,7 @@ fun HelpAndSupportView(
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_contact)) {
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_bigtop_updates), stringResource(MR.strings.follow_simplex_network_news), { uriHandler.openVerifiedSimplexUri(simplexNewsUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped)
|
||||
SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped)
|
||||
}
|
||||
SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary)
|
||||
|
||||
@@ -1035,6 +1035,7 @@
|
||||
<string name="markdown_in_messages">Markdown in messages</string>
|
||||
<string name="chat_with_the_founder">Send questions and ideas</string>
|
||||
<string name="send_us_an_email">Send us email</string>
|
||||
<string name="follow_simplex_network_news">Follow SimpleX Network News</string>
|
||||
<string name="chat_lock">SimpleX Lock</string>
|
||||
<string name="chat_console">Chat console</string>
|
||||
<string name="message_servers">Message servers</string>
|
||||
@@ -2745,6 +2746,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">Invest in SimpleX Chat!</string>
|
||||
<string name="invest_banner_subtitle" translatable="false">Crowdfunding on Wefunder</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>
|
||||
|
||||
@@ -24,11 +24,11 @@ android.nonTransitiveRClass=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=7.0.2
|
||||
android.version_code=375
|
||||
android.version_name=7.0.3
|
||||
android.version_code=381
|
||||
|
||||
desktop.version_name=7.0.2
|
||||
desktop.version_code=159
|
||||
desktop.version_name=7.0.3
|
||||
desktop.version_code=164
|
||||
|
||||
kotlin.version=2.1.20
|
||||
gradle.plugin.version=8.7.0
|
||||
|
||||
@@ -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