mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 02:48:54 +00:00
Merge branch 'stable'
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user