ui: new UX for making connections after / as part of onboarding (#6753)

* ui: additional images, views for making connections and creating groups (#6750)

* ios: setup for additional assets

* ios build config

* header

* fix

* update layout

* more views with images

* layout

* layout

* android images and view

* fix path

* fix desktop

* fix desktop build

* smaller image

* layout

* more layout

* more kotlin views

* group layout

* padding

* create group layout

* more create group layout

* layout

* tweak layout

* more tweak

* config

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* ios: connecting as part of onboarding (#6754)

* ios: implementation of "connecting" cards

* ios: revision

* fix flip

* fixes

* fix frame

* replace nav stack with tab view

* rename

* update gradient and card label material

* fix gradient

* debug

* remove debug code

* update card labels

* card label layout

* landscape cards

* layout

* safe area

* less bold

* debug landscape

* refactor titles, back inline with title in landscape

* remove ignoreSafeArea

* remove extra padding

* refactor

* clean

* layout spec added to plan

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* android, desktop: connecting during onboarding - new cards (#6757)

* android, desktop: connecting during onboarding - new cards

* fix

* change layout

* fixes

* fix

* fix

* layout

* fix layout

* animation

* import

* paddings

* 350ms

* font

* fonts

* layout

* box

* more layout

* layout

* simpler

* hide toolbar heading in onboarding mode

* simpler desktop layout

* better desktop

* revert desktop toolbar

* bigger font, landscape

* fix desktop

* cap width

* refactor, simplify

* qr code scanner icon

* use icon without assets

* cleaner

* fix

* fix

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* android, desktop: connect banner after onboarding (#6761)

* android, desktop: connect banner after onboarding

* improve

* smaller button

* bigger icon, same string

* fallback gradients

* improve build

* simpler connect screens during onboarding

* left-align

* update strings

* improve state machine

* text, padding

* strings

* primary color for tap to paste link

* fix race condition

* fix loading race

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* ios: banner and connect screens (#6767)

* ios: banner and connect screens

* fix

* return nav

* remove padding

* refactor

* refactor

* refactor 2

* refactor 3

* refactor 4

* header

* xcode files

* improve

* fix toolbar

* toolbar 2

* no assets

* no assets 2

* padding

* android padding

* simplify

* layout

* fix

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* fix refreshable

* text

* fix toolbar color

* rework address share logic

* padding

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
This commit is contained in:
Evgeny
2026-04-21 17:41:52 +01:00
committed by GitHub
parent 6f86c5af2e
commit 035a2f954c
62 changed files with 2858 additions and 407 deletions
@@ -34,6 +34,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.helpers.ModalManager.Companion.fromEndToStartTransition
import chat.simplex.common.views.helpers.ModalManager.Companion.fromStartToEndTransition
import chat.simplex.common.views.localauth.VerticalDivider
import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.onboarding.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
@@ -383,7 +384,9 @@ fun CenterPartOfScreen() {
}
when (currentChatId.value) {
null -> {
if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) {
if (shouldShowOnboarding()) {
ConnectOnboardingView()
} else if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) {
Box(
Modifier
.fillMaxSize()
@@ -1157,10 +1157,10 @@ object ChatModel {
showingInvitation.value = null
chatsContext.chatItems.clearAndNotify()
chatModel.chatId.value = withId
ModalManager.start.closeModals()
ModalManager.end.closeModals()
}
}
ModalManager.start.closeModals()
ModalManager.end.closeModals()
}
}
@@ -626,6 +626,7 @@ val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp
val DEFAULT_MIN_SECTION_ITEM_HEIGHT = 50.dp
val DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL = 15.dp
val DEFAULT_WINDOW_WIDTH = 1366.dp
val DEFAULT_START_MODAL_WIDTH = 388.dp
val DEFAULT_MIN_CENTER_MODAL_WIDTH = 590.dp
val DEFAULT_END_MODAL_WIDTH = 388.dp
@@ -24,7 +24,13 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntSize
import chat.simplex.common.AppLock
import chat.simplex.common.BuildConfigCommon
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
@@ -234,53 +240,120 @@ private fun ChatListCard(
}
}
private const val BANNER_IMAGE_RATIO = 800f / 505f
@Composable
private fun AddressCreationCard() {
ChatListCard(
close = {
appPrefs.addressCreationCardShown.set(true)
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.simplex_address),
text = generalGetString(MR.strings.address_creation_instruction),
private fun BannerGradientBox(isDark: Boolean, content: @Composable () -> Unit) {
val stops = if (isDark) darkStops else lightStops
val scale = if (isDark) 1.5f else 1.2f
val gp = gradientPoints(1f / BANNER_IMAGE_RATIO, scale)
var size by remember { mutableStateOf(IntSize.Zero) }
val brush = remember(size, isDark) {
if (size.width > 0 && size.height > 0) {
Brush.linearGradient(
colorStops = stops,
start = Offset(gp.startX * size.width, gp.startY * size.height),
end = Offset(gp.endX * size.width, gp.endY * size.height)
)
},
onCardClick = {
ModalManager.start.showModal {
UserAddressLearnMore(showCreateAddressButton = true)
}
} else {
Brush.linearGradient(colorStops = stops)
}
) {
Box(modifier = Modifier.matchParentSize().padding(end = (DEFAULT_PADDING_HALF + 2.dp) * fontSizeSqrtMultiplier, bottom = 2.dp), contentAlignment = Alignment.BottomEnd) {
TextButton(
onClick = {
ModalManager.start.showModalCloseable { close ->
UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, close = close)
}
},
) {
Text(stringResource(MR.strings.create_address_button), style = MaterialTheme.typography.body1)
}
}
Box(
Modifier.fillMaxWidth().aspectRatio(BANNER_IMAGE_RATIO).background(brush).onSizeChanged { size = it },
contentAlignment = Alignment.Center
) { content() }
}
@Composable
private fun ConnectBannerCard() {
val isDark = isInDarkTheme()
val labelBg = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
.copy(alpha = appPrefs.inAppBarsAlpha.get())
val buttonSize = 30.dp * fontSizeSqrtMultiplier
val gap = 3.dp * fontSizeSqrtMultiplier
Column(horizontalAlignment = Alignment.End) {
IconButton(
onClick = { appPrefs.addressCreationCardShown.set(true) },
modifier = Modifier.size(buttonSize)
) {
Icon(
painterResource(MR.images.ic_close),
contentDescription = stringResource(MR.strings.icon_descr_close_button),
modifier = Modifier
.size(buttonSize)
.background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.92f), CircleShape)
.padding(buttonSize * 0.15f),
tint = MaterialTheme.colors.secondary
)
}
Spacer(Modifier.height(gap))
Row(
Modifier
.fillMaxWidth()
.padding(DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically
.height(IntrinsicSize.Min)
.clip(RoundedCornerShape(18.dp))
) {
Box(Modifier.padding(vertical = 4.dp)) {
Box(Modifier.background(MaterialTheme.colors.primary, CircleShape).padding(12.dp)) {
ProfileImage(size = 37.dp, null, icon = MR.images.ic_mail_filled, color = Color.White, backgroundColor = Color.Red)
Column(
Modifier.weight(1f).clickable {
ModalManager.start.showModalCloseable { close ->
NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close)
}
}
) {
if (BuildConfigCommon.SIMPLEX_ASSETS) {
Image(
painterResource(if (isDark) MR.images.banner_create_link_light else MR.images.banner_create_link),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
} else {
BannerGradientBox(isDark) {
Icon(painterResource(MR.images.ic_add_link), contentDescription = null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colors.primary)
}
}
Box(Modifier.fillMaxWidth().background(labelBg).padding(vertical = 8.dp), contentAlignment = Alignment.Center) {
if (BuildConfigCommon.SIMPLEX_ASSETS) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(painterResource(MR.images.ic_add_link), contentDescription = null, modifier = Modifier.size(18.dp), tint = MaterialTheme.colors.primary)
Text(stringResource(MR.strings.new_1_time_link), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onBackground)
}
} else {
Text(stringResource(MR.strings.new_1_time_link), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onBackground)
}
}
}
Column(modifier = Modifier.padding(start = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.your_simplex_contact_address), style = MaterialTheme.typography.h3)
Spacer(Modifier.fillMaxWidth().padding(DEFAULT_PADDING_HALF))
Row(verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(MR.strings.how_to_use_simplex_chat), Modifier.padding(end = DEFAULT_SPACE_AFTER_ICON), style = MaterialTheme.typography.body1)
Icon(
painterResource(MR.images.ic_info),
null,
Spacer(Modifier.width(2.dp).fillMaxHeight().background(MaterialTheme.colors.background))
Column(
Modifier.weight(1f).clickable {
ModalManager.start.showModalCloseable { close ->
NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = close)
}
}
) {
if (BuildConfigCommon.SIMPLEX_ASSETS) {
Image(
painterResource(if (isDark) MR.images.banner_paste_link_light else MR.images.banner_paste_link),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
} else {
BannerGradientBox(isDark) {
Icon(painterResource(MR.images.ic_qr_code_scanner), contentDescription = null, modifier = Modifier.size(40.dp), tint = MaterialTheme.colors.primary)
}
}
Box(Modifier.fillMaxWidth().background(labelBg).padding(vertical = 8.dp), contentAlignment = Alignment.Center) {
if (BuildConfigCommon.SIMPLEX_ASSETS) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(painterResource(MR.images.ic_qr_code_scanner), contentDescription = null, modifier = Modifier.size(18.dp), tint = MaterialTheme.colors.primary)
Text(stringResource(if (appPlatform.isAndroid) MR.strings.scan_paste_link else MR.strings.paste_link), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onBackground)
}
} else {
Text(stringResource(if (appPlatform.isAndroid) MR.strings.scan_paste_link else MR.strings.paste_link), style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onBackground)
}
}
}
}
@@ -289,15 +362,31 @@ private fun AddressCreationCard() {
@Composable
private fun BoxScope.ChatListWithLoadingScreen(searchText: MutableState<TextFieldValue>, listState: LazyListState) {
if (!chatModel.desktopNoUserNoRemote) {
ChatList(searchText = searchText, listState)
if (chatModel.chatRunning.value == null) {
Text(stringResource(MR.strings.loading_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
} else if (shouldShowOnboarding()) {
if (appPlatform.isAndroid) AndroidOnboardingCards()
} else {
if (!chatModel.desktopNoUserNoRemote) {
ChatList(searchText = searchText, listState)
}
if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
Text(stringResource(MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary)
}
}
if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
Text(
stringResource(
if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats
), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary
)
}
@Composable
private fun AndroidOnboardingCards() {
val oneHandUI = remember { appPrefs.oneHandUI.state }
val topPad = topPaddingToContent(false)
val bottomPad = if (oneHandUI.value) {
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier
} else {
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
}
Box(Modifier.fillMaxSize().padding(top = topPad, bottom = bottomPad)) {
ConnectOnboardingView()
}
}
@@ -454,31 +543,33 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
}
},
title = {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) {
Text(
stringResource(MR.strings.your_chats),
color = MaterialTheme.colors.onBackground,
fontWeight = FontWeight.SemiBold,
)
SubscriptionStatusIndicator(
click = {
ModalManager.start.closeModals()
val summary = serversSummary.value
ModalManager.start.showModalCloseable(
endButtons = {
if (summary != null) {
ShareButton {
val json = Json {
prettyPrint = true
if (!shouldShowOnboarding()) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) {
Text(
stringResource(MR.strings.your_chats),
color = MaterialTheme.colors.onBackground,
fontWeight = FontWeight.SemiBold,
)
SubscriptionStatusIndicator(
click = {
ModalManager.start.closeModals()
val summary = serversSummary.value
ModalManager.start.showModalCloseable(
endButtons = {
if (summary != null) {
ShareButton {
val json = Json {
prettyPrint = true
}
val text = json.encodeToString(PresentedServersSummary.serializer(), summary)
clipboard.shareText(text)
}
val text = json.encodeToString(PresentedServersSummary.serializer(), summary)
clipboard.shareText(text)
}
}
}
) { ServersSummaryView(chatModel.currentRemoteHost.value, serversSummary) }
}
)
) { ServersSummaryView(chatModel.currentRemoteHost.value, serversSummary) }
}
)
}
}
},
onTitleClick = if (canScrollToZero.value) { { scrollToBottom(scope, listState) } } else null,
@@ -860,14 +951,6 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
}
}
if (!addressCreationCardShown.value) {
LaunchedEffect(chatModel.userAddress.value) {
if (chatModel.userAddress.value != null) {
appPrefs.addressCreationCardShown.set(true)
}
}
}
LaunchedEffect(activeFilter.value) {
searchText.value = TextFieldValue("")
}
@@ -914,8 +997,8 @@ private fun ChatListFeatureCards() {
if (!oneHandUICardShown.value && !oneHandUI.value) {
ToggleChatListCard()
}
if (!addressCreationCardShown.value) {
AddressCreationCard()
if (!addressCreationCardShown.value && hasConversations(chatModel.chats.value)) {
ConnectBannerCard()
}
if (!oneHandUICardShown.value && oneHandUI.value) {
ToggleChatListCard()
@@ -64,7 +64,7 @@ private fun Modifier.androidBlurredModifier(
}
}
.drawBehind {
drawRect(Color.Black)
drawRect(CurrentColors.value.colors.background)
if (onTop) {
clipRect {
if (backgroundGraphicsLayer.size != IntSize.Zero) {
@@ -110,7 +110,7 @@ private fun Modifier.desktopBlurredModifier(
clip = blurRadius.value > 0
}
.drawBehind {
drawRect(Color.Black)
drawRect(CurrentColors.value.colors.background)
if (onTop) {
clipRect {
if (backgroundGraphicsLayer.size != IntSize.Zero) {
@@ -27,6 +27,8 @@ import chat.simplex.common.views.*
import chat.simplex.common.views.chat.group.GroupLinkView
import chat.simplex.common.views.chatlist.openGroupChat
import chat.simplex.common.views.usersettings.*
import androidx.compose.ui.layout.ContentScale
import chat.simplex.common.BuildConfigCommon
import chat.simplex.res.MR
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -99,22 +101,33 @@ fun AddGroupLayout(
) {
ModalView(close = close) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId))
Box(
AppBarTitle(stringResource(MR.strings.create_secret_group_title), hostDevice(rhId), bottomPadding = DEFAULT_PADDING_HALF)
Row(
Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
contentAlignment = Alignment.Center
.padding(vertical = DEFAULT_PADDING_HALF),
horizontalArrangement = if (BuildConfigCommon.SIMPLEX_ASSETS) Arrangement.SpaceEvenly else Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Box(contentAlignment = Alignment.TopEnd) {
Box(contentAlignment = Alignment.Center) {
ProfileImage(108.dp, image = profileImage.value, icon = MR.images.ic_supervised_user_circle_filled)
EditImageButton { scope.launch { bottomSheetModalState.show() } }
}
if (profileImage.value != null) {
DeleteImageButton { profileImage.value = null }
Box(contentAlignment = Alignment.Center) {
Box(contentAlignment = Alignment.TopEnd) {
Box(contentAlignment = Alignment.Center) {
ProfileImage(128.dp, image = profileImage.value, icon = MR.images.ic_supervised_user_circle_filled)
EditImageButton { scope.launch { bottomSheetModalState.show() } }
}
if (profileImage.value != null) {
DeleteImageButton { profileImage.value = null }
}
}
}
if (BuildConfigCommon.SIMPLEX_ASSETS) {
Image(
painterResource(if (isInDarkTheme()) MR.images.create_group_light else MR.images.create_group),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.height(140.dp)
)
}
}
Row(Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
@@ -74,7 +74,7 @@ fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
Column(Modifier.align(Alignment.BottomCenter)) {
DefaultAppBar(
navigationButton = { NavigationButtonBack(onButtonClicked = close) },
fixedTitleText = generalGetString(MR.strings.new_message),
fixedTitleText = generalGetString(MR.strings.new_chat),
onTop = false,
)
}
@@ -359,7 +359,7 @@ private fun ModalData.NewChatSheetLayout(
item {
Box(Modifier.padding(top = blankSpaceSize)) {
AppBarTitle(
stringResource(MR.strings.new_message),
stringResource(MR.strings.new_chat),
hostDevice(rh?.remoteHostId),
bottomPadding = DEFAULT_PADDING
)
@@ -21,9 +21,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
@@ -39,6 +41,7 @@ import chat.simplex.common.views.chat.item.CIFileViewScope
import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.common.BuildConfigCommon
import chat.simplex.res.MR
import kotlinx.coroutines.*
@@ -47,7 +50,7 @@ enum class NewChatOption {
}
@Composable
fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRCodeScanner: Boolean = false, close: () -> Unit) {
fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRCodeScanner: Boolean = false, onboarding: Boolean = false, close: () -> Unit) {
val selection = remember { stateGetOrPut("selection") { selection } }
val showQRCodeScanner = remember { stateGetOrPut("showQRCodeScanner") { showQRCodeScanner } }
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) }
@@ -104,60 +107,71 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
}
}
BoxWithConstraints {
if (onboarding) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = DEFAULT_PADDING)
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState(
initialPage = selection.value.ordinal,
initialPageOffsetFraction = 0f
) { NewChatOption.values().size }
KeyChangeEffect(pagerState.currentPage) {
selection.value = NewChatOption.values()[pagerState.currentPage]
Spacer(Modifier.height(DEFAULT_PADDING))
when (selection.value) {
NewChatOption.INVITE -> PrepareAndInviteView(rh?.remoteHostId, contactConnection, connLinkInvitation, creatingConnReq, onboarding = true)
NewChatOption.CONNECT -> ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close, onboarding = true)
}
TabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = Color.Transparent,
contentColor = MaterialTheme.colors.primary,
) {
tabTitles.forEachIndexed { index, it ->
LeadingIconTab(
selected = pagerState.currentPage == index,
onClick = {
scope.launch {
pagerState.animateScrollToPage(index)
}
},
text = { Text(it, fontSize = 13.sp) },
icon = {
Icon(
if (NewChatOption.INVITE.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_qr_code),
it
)
},
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = MaterialTheme.colors.secondary,
)
SectionBottomSpacer()
}
} else {
BoxWithConstraints {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = DEFAULT_PADDING)
val scope = rememberCoroutineScope()
val pagerState = rememberPagerState(
initialPage = selection.value.ordinal,
initialPageOffsetFraction = 0f
) { NewChatOption.values().size }
KeyChangeEffect(pagerState.currentPage) {
selection.value = NewChatOption.values()[pagerState.currentPage]
}
}
HorizontalPager(state = pagerState, Modifier, pageNestedScrollConnection = LocalAppBarHandler.current!!.connection, verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index ->
Column(
Modifier
.fillMaxWidth()
.heightIn(min = this@BoxWithConstraints.maxHeight - 150.dp),
verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connLinkInvitation.connFullLink.isEmpty()) Arrangement.Center else Arrangement.Top
TabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = Color.Transparent,
contentColor = MaterialTheme.colors.primary,
) {
Spacer(Modifier.height(DEFAULT_PADDING))
when (index) {
NewChatOption.INVITE.ordinal -> {
PrepareAndInviteView(rh?.remoteHostId, contactConnection, connLinkInvitation, creatingConnReq)
}
NewChatOption.CONNECT.ordinal -> {
ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close)
}
tabTitles.forEachIndexed { index, it ->
LeadingIconTab(
selected = pagerState.currentPage == index,
onClick = {
scope.launch {
pagerState.animateScrollToPage(index)
}
},
text = { Text(it, fontSize = 13.sp) },
icon = {
Icon(
if (NewChatOption.INVITE.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_qr_code),
it
)
},
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = MaterialTheme.colors.secondary,
)
}
}
HorizontalPager(state = pagerState, Modifier, pageNestedScrollConnection = LocalAppBarHandler.current!!.connection, verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index ->
Column(
Modifier
.fillMaxWidth()
.heightIn(min = this@BoxWithConstraints.maxHeight - 150.dp),
verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connLinkInvitation.connFullLink.isEmpty()) Arrangement.Center else Arrangement.Top
) {
Spacer(Modifier.height(DEFAULT_PADDING))
when (index) {
NewChatOption.INVITE.ordinal -> {
PrepareAndInviteView(rh?.remoteHostId, contactConnection, connLinkInvitation, creatingConnReq)
}
NewChatOption.CONNECT.ordinal -> {
ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close)
}
}
SectionBottomSpacer()
}
SectionBottomSpacer()
}
}
}
@@ -165,12 +179,13 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
}
@Composable
private fun PrepareAndInviteView(rhId: Long?, contactConnection: MutableState<PendingContactConnection?>, connLinkInvitation: CreatedConnLink, creatingConnReq: MutableState<Boolean>) {
private fun PrepareAndInviteView(rhId: Long?, contactConnection: MutableState<PendingContactConnection?>, connLinkInvitation: CreatedConnLink, creatingConnReq: MutableState<Boolean>, onboarding: Boolean = false) {
if (connLinkInvitation.connFullLink.isNotEmpty()) {
InviteView(
rhId,
connLinkInvitation = connLinkInvitation,
contactConnection = contactConnection,
onboarding = onboarding,
)
} else if (creatingConnReq.value) {
CreatingLinkProgressView()
@@ -448,23 +463,53 @@ fun ActiveProfilePicker(
}
@Composable
private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contactConnection: MutableState<PendingContactConnection?>) {
private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contactConnection: MutableState<PendingContactConnection?>, onboarding: Boolean = false) {
val showShortLink = remember { mutableStateOf(true) }
Spacer(Modifier.height(10.dp))
SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) {
if (BuildConfigCommon.SIMPLEX_ASSETS) {
Image(
painterResource(if (isInDarkTheme()) {
if (onboarding) MR.images.one_time_link_light else MR.images.one_time_link_small_light
} else {
if (onboarding) MR.images.one_time_link else MR.images.one_time_link_small
}),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxWidth()
)
} else {
Spacer(Modifier.height(10.dp))
}
if (onboarding) {
Text(
stringResource(MR.strings.onboarding_send_1_time_link),
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF),
style = MaterialTheme.typography.body1
)
LinkTextView(connLinkInvitation.simplexChatUri(short = showShortLink.value), true)
}
Spacer(Modifier.height(DEFAULT_PADDING))
SectionViewWithButton(
stringResource(MR.strings.or_show_this_qr_code).uppercase(),
titleButton = if (connLinkInvitation.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null
) {
Text(
stringResource(MR.strings.onboarding_or_show_qr_code),
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF),
style = MaterialTheme.typography.body1
)
SimpleXCreatedLinkQRCode(connLinkInvitation, short = showShortLink.value, onShare = { chatModel.markShowingInvitationUsed() })
} else {
SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) {
LinkTextView(connLinkInvitation.simplexChatUri(short = showShortLink.value), true)
}
Spacer(Modifier.height(DEFAULT_PADDING))
SectionViewWithButton(
stringResource(MR.strings.or_show_this_qr_code).uppercase(),
titleButton = if (connLinkInvitation.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null
) {
SimpleXCreatedLinkQRCode(connLinkInvitation, short = showShortLink.value, onShare = { chatModel.markShowingInvitationUsed() })
}
}
if (!onboarding) {
Spacer(Modifier.height(DEFAULT_PADDING))
val incognito by remember(chatModel.showingInvitation.value?.conn?.incognito, controller.appPrefs.incognito.get()) {
derivedStateOf {
@@ -531,6 +576,7 @@ private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contact
SectionTextFooter(generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared))
}
}
}
}
@Composable
@@ -577,13 +623,26 @@ fun AddContactLearnMoreButton() {
}
@Composable
private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState<Boolean>, pastedLink: MutableState<String>, close: () -> Unit) {
private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState<Boolean>, pastedLink: MutableState<String>, close: () -> Unit, onboarding: Boolean = false) {
DisposableEffect(Unit) {
onDispose {
connectProgressManager.cancelConnectProgress()
}
}
if (BuildConfigCommon.SIMPLEX_ASSETS) {
Image(
painterResource(if (isInDarkTheme()) {
if (onboarding) MR.images.connect_via_link_light else MR.images.connect_via_link_small_light
} else {
if (onboarding) MR.images.connect_via_link else MR.images.connect_via_link_small
}),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxWidth()
)
}
SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) {
PasteLinkView(rhId, pastedLink, showQRCodeScanner, close)
}
@@ -625,7 +684,7 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState<String>, showQRC
}
}) {
Box(Modifier.weight(1f)) {
Text(stringResource(MR.strings.tap_to_paste_link))
Text(stringResource(MR.strings.tap_to_paste_link), color = MaterialTheme.colors.primary)
}
if (connectProgressManager.showConnectProgress != null) {
CIFileViewScope.progressIndicator(sizeMultiplier = 0.6f)
@@ -681,6 +740,13 @@ fun LinkTextView(link: String, share: Boolean) {
// So using BasicTextField + manual ...
Text("", fontSize = 16.sp)
if (share) {
Spacer(Modifier.width(DEFAULT_PADDING))
IconButton({
chatModel.markShowingInvitationUsed()
clipboard.setText(AnnotatedString(link))
}, Modifier.size(20.dp)) {
Icon(painterResource(MR.images.ic_content_copy), null, tint = MaterialTheme.colors.primary)
}
Spacer(Modifier.width(DEFAULT_PADDING))
IconButton({
chatModel.markShowingInvitationUsed()
@@ -0,0 +1,422 @@
package chat.simplex.common.views.newchat
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
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.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
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.TextAlign
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.BuildConfigCommon
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.helpers.*
import chat.simplex.common.views.usersettings.UserAddressView
import chat.simplex.res.MR
import kotlinx.coroutines.launch
import kotlin.math.cos
import kotlin.math.sin
private const val CARD_HEIGHT_RATIO = 0.75f
private const val GRADIENT_ANGLE_RAD = 80.0 * Math.PI / 180.0
@Composable
fun shouldShowOnboarding(): Boolean {
val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state }
val chats = chatModel.chats.value
return !addressCreationCardShown.value && chats.isNotEmpty() && !hasConversations(chats)
}
fun hasConversations(chats: List<Chat>): Boolean =
chats.any { chat ->
when (val c = chat.chatInfo) {
is ChatInfo.Local -> false
is ChatInfo.Direct -> !c.contact.chatDeleted && !c.contact.isContactCard
is ChatInfo.Group -> true
is ChatInfo.ContactRequest -> false
is ChatInfo.ContactConnection -> false
is ChatInfo.InvalidJSON -> false
}
}
internal data class GradientEndpoints(val startX: Float, val startY: Float, val endX: Float, val endY: Float)
internal fun gradientPoints(aspectRatio: Float, scale: Float): GradientEndpoints {
val r = aspectRatio.toDouble()
val s = scale.toDouble()
val dx = cos(GRADIENT_ANGLE_RAD)
val dy = -sin(GRADIENT_ANGLE_RAD) / r
val dLenSq = dx * dx + dy * dy
val projections = doubleArrayOf(
-0.5 * dx + (-0.5) * dy,
0.5 * dx + (-0.5) * dy,
-0.5 * dx + 0.5 * dy,
0.5 * dx + 0.5 * dy
)
val tMin = projections.min()
val tMax = projections.max()
val startX = 0.5 + tMin * dx / dLenSq
val startY = 0.5 + tMin * dy / dLenSq
val endX = 0.5 + tMax * dx / dLenSq
val endY = 0.5 + tMax * dy / dLenSq
return GradientEndpoints(
startX = (0.5 + (startX - 0.5) * s).toFloat(),
startY = (0.5 + (startY - 0.5) * s).toFloat(),
endX = (0.5 + (endX - 0.5) * s).toFloat(),
endY = (0.5 + (endY - 0.5) * s).toFloat()
)
}
internal val lightStops = arrayOf(
0.0f to Color(0xFFd2e8ff),
0.5f to Color(0xFFcce9ff),
0.9f to Color(0xFFdfffff),
1.0f to Color(0xFFfffcea)
)
internal val darkStops = arrayOf(
0.4f to Color(0xFF040a24),
0.72f to Color(0xFF3854ab),
0.9f to Color(0xFFa8edf3),
1.0f to Color(0xFFfff6e0)
)
private fun Modifier.maxHeightByWidthRatio(ratio: Float) = layout { measurable, constraints ->
val maxH = (constraints.maxWidth * ratio).toInt().coerceAtMost(constraints.maxHeight)
val placeable = measurable.measure(constraints.copy(minHeight = 0, maxHeight = maxH))
layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
}
@Composable
fun OnboardingCardView(
imageName: dev.icerock.moko.resources.ImageResource,
imageNameLight: dev.icerock.moko.resources.ImageResource,
icon: dev.icerock.moko.resources.ImageResource,
title: String,
subtitle: String? = null,
labelHeightRatio: Float,
onClick: () -> Unit
) {
var imageAreaSize by remember { mutableStateOf(IntSize.Zero) }
val isDark = isInDarkTheme()
val stops = if (isDark) darkStops else lightStops
val scale = if (isDark) 1.5f else 1.2f
val brush = remember(imageAreaSize, isDark) {
if (imageAreaSize.width > 0 && imageAreaSize.height > 0) {
val aspect = imageAreaSize.height.toFloat() / imageAreaSize.width.toFloat()
val gp = gradientPoints(aspect, scale)
Brush.linearGradient(
colorStops = stops,
start = Offset(gp.startX * imageAreaSize.width, gp.startY * imageAreaSize.height),
end = Offset(gp.endX * imageAreaSize.width, gp.endY * imageAreaSize.height)
)
} else {
Brush.linearGradient(colorStops = stops)
}
}
val labelBg = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)
.copy(alpha = appPrefs.inAppBarsAlpha.get())
Box(
Modifier
.fillMaxSize()
.clip(RoundedCornerShape(24.dp))
.clickable(onClick = onClick)
) {
Column(Modifier.fillMaxSize()) {
Box(
Modifier
.fillMaxWidth()
.weight(1f)
.background(brush)
.onSizeChanged { imageAreaSize = it }
) {
if (BuildConfigCommon.SIMPLEX_ASSETS) {
Image(
painterResource(if (isDark) imageNameLight else imageName),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxSize()
)
} else {
Icon(
painterResource(icon),
contentDescription = null,
modifier = Modifier.size(64.dp).align(Alignment.Center),
tint = MaterialTheme.colors.primary
)
}
}
Box(
Modifier
.fillMaxWidth()
.aspectRatio(1f / labelHeightRatio)
.background(labelBg),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (BuildConfigCommon.SIMPLEX_ASSETS) {
Icon(
painterResource(icon),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary
)
}
Text(
title,
style = (if (appPlatform.isDesktop) MaterialTheme.typography.h3 else MaterialTheme.typography.h4).copy(fontWeight = FontWeight.Medium),
color = MaterialTheme.colors.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (subtitle != null) {
Text(
subtitle,
style = if (appPlatform.isDesktop) MaterialTheme.typography.body1 else MaterialTheme.typography.body2,
color = MaterialTheme.colors.onBackground.copy(alpha = 0.7f)
)
}
}
}
}
}
}
@Composable
private fun PageHeader(title: String, isLandscape: Boolean, onBack: (() -> Unit)? = null) {
val color = if (onBack != null) MaterialTheme.colors.primary else Color.Transparent
val baseStyle = MaterialTheme.typography.h1
val titleView = @Composable {
var fontScale by remember(title) { mutableStateOf(1f) }
Text(
title,
style = baseStyle.copy(fontSize = baseStyle.fontSize * fontScale),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
onTextLayout = { result ->
if (result.hasVisualOverflow && fontScale > 0.5f) {
fontScale -= 0.05f
}
}
)
}
if (isLandscape) {
Box(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING)) {
BackButton(Modifier.align(Alignment.CenterStart), color, onBack)
titleView()
}
} else {
Column(Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING)) {
Box(Modifier.align(Alignment.Start)) {
BackButton(color = color, onClick = onBack)
}
titleView()
}
}
}
@Composable
private fun BackButton(modifier: Modifier = Modifier, color: Color = MaterialTheme.colors.primary, onClick: (() -> Unit)? = null) {
Row(
modifier
.clip(RoundedCornerShape(20.dp))
.clickable(enabled = onClick != null, onClick = onClick ?: {})
.padding(end = 12.dp, top = 10.dp, bottom = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(
painterResource(MR.images.ic_arrow_back_ios_new),
contentDescription = stringResource(MR.strings.back),
tint = color,
modifier = Modifier.height(24.dp)
)
Text(stringResource(MR.strings.back), color = color)
}
}
@Composable
private fun CardPair(
isLandscape: Boolean,
heightRatio: Float,
card1: @Composable () -> Unit,
card2: @Composable () -> Unit
) {
if (isLandscape) {
Row(
Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically
) {
Box(Modifier.weight(1f).maxHeightByWidthRatio(heightRatio)) { card1() }
Box(Modifier.weight(1f).maxHeightByWidthRatio(heightRatio)) { card2() }
}
} else {
Column(
Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING, Alignment.CenterVertically)
) {
Box(Modifier.fillMaxWidth().weight(1f, fill = false).maxHeightByWidthRatio(heightRatio)) { card1() }
Box(Modifier.fillMaxWidth().weight(1f, fill = false).maxHeightByWidthRatio(heightRatio)) { card2() }
}
}
}
@Composable
private fun OnboardingPageLayout(
title: String,
onBack: (() -> Unit)? = null,
cards: @Composable (isLandscape: Boolean) -> Unit
) {
val isLandscape = appPlatform.isDesktop || windowOrientation() == WindowOrientation.LANDSCAPE
Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
PageHeader(title = title, isLandscape = isLandscape, onBack = onBack)
Box(Modifier.weight(1f).fillMaxWidth().padding(vertical = DEFAULT_PADDING)) {
cards(isLandscape)
}
}
}
@Composable
fun ConnectOnboardingView() {
val pagerState = rememberPagerState(initialPage = 0) { 2 }
val scope = rememberCoroutineScope()
val startModalsOpen = appPlatform.isDesktop && ModalManager.start.hasModalsOpen
val cardAlpha by animateFloatAsState(if (startModalsOpen) 0.3f else 1f)
val cardClickOverride: (() -> Unit)? = if (startModalsOpen) {
{ ModalManager.start.closeModals() }
} else null
fun goToPage(target: Int) {
if (appPlatform.isDesktop) {
scope.launch { pagerState.scrollToPage(target) }
} else {
scope.launch { pagerState.animateScrollToPage(target, animationSpec = tween(350)) }
}
}
val pager = @Composable {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxWidth(),
userScrollEnabled = !appPlatform.isDesktop
) { page ->
when (page) {
0 -> OnboardingPageLayout(title = stringResource(MR.strings.talk_to_someone)) { isLandscape ->
CardPair(isLandscape, CARD_HEIGHT_RATIO,
card1 = {
OnboardingCardView(
imageName = MR.images.card_let_someone_connect_to_you_alpha,
imageNameLight = MR.images.card_let_someone_connect_to_you_alpha_light,
icon = MR.images.ic_add_link,
title = stringResource(MR.strings.let_someone_connect_to_you),
labelHeightRatio = 0.132f,
onClick = cardClickOverride ?: { goToPage(1) }
)
},
card2 = {
OnboardingCardView(
imageName = MR.images.card_connect_via_link_alpha,
imageNameLight = MR.images.card_connect_via_link_alpha_light,
icon = MR.images.ic_qr_code_scanner,
title = stringResource(MR.strings.connect_via_link_or_qr_code),
labelHeightRatio = 0.132f,
onClick = cardClickOverride ?: {
ModalManager.start.showModalCloseable { close ->
NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, onboarding = true, close = close)
}
}
)
}
)
}
1 -> OnboardingPageLayout(
title = stringResource(MR.strings.connect_with_someone),
onBack = cardClickOverride ?: { goToPage(0) }
) { isLandscape ->
CardPair(isLandscape, CARD_HEIGHT_RATIO,
card1 = {
OnboardingCardView(
imageName = MR.images.card_invite_someone_privately_alpha,
imageNameLight = MR.images.card_invite_someone_privately_alpha_light,
icon = MR.images.ic_add_link,
title = stringResource(MR.strings.invite_someone_privately),
subtitle = stringResource(MR.strings.a_link_for_one_person),
labelHeightRatio = 0.195f,
onClick = cardClickOverride ?: {
ModalManager.start.showModalCloseable { close ->
NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, onboarding = true, close = close)
}
}
)
},
card2 = {
OnboardingCardView(
imageName = MR.images.card_create_your_public_address_alpha,
imageNameLight = MR.images.card_create_your_public_address_alpha_light,
icon = MR.images.ic_qr_code,
title = stringResource(if (chatModel.userAddress.value != null) MR.strings.your_public_address else MR.strings.create_your_public_address),
subtitle = stringResource(MR.strings.for_anyone_to_reach_you),
labelHeightRatio = 0.195f,
onClick = cardClickOverride ?: {
ModalManager.start.showModalCloseable { close ->
UserAddressView(chatModel = chatModel, shareViaProfile = false, autoCreateAddress = true, onboarding = true, close = close)
}
}
)
}
)
}
}
}
}
if (appPlatform.isDesktop) {
val maxContentWidth = DEFAULT_WINDOW_WIDTH - DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier
Box(
Modifier.fillMaxSize().background(MaterialTheme.colors.background).padding(vertical = DEFAULT_PADDING).graphicsLayer { alpha = cardAlpha },
contentAlignment = Alignment.Center
) {
Box(Modifier.widthIn(max = maxContentWidth).fillMaxHeight()) {
pager()
}
}
} else {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
pager()
}
}
}
@@ -7,7 +7,9 @@ import SectionTextFooter
import SectionView
import SectionViewWithButton
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
@@ -28,6 +30,7 @@ import chat.simplex.common.model.MsgContent
import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.newchat.*
import chat.simplex.common.BuildConfigCommon
import chat.simplex.res.MR
@Composable
@@ -35,6 +38,7 @@ fun UserAddressView(
chatModel: ChatModel,
shareViaProfile: Boolean = false,
autoCreateAddress: Boolean = false,
onboarding: Boolean = false,
close: () -> Unit
) {
// TODO close when remote host changes
@@ -75,17 +79,31 @@ fun UserAddressView(
addressSettings = AddressSettings(businessAddress = false, autoAccept = null, autoReply = null)
)
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.share_address_with_contacts_question),
text = generalGetString(MR.strings.add_address_to_your_profile),
confirmText = generalGetString(MR.strings.share_verb),
onConfirm = {
setProfileAddress(true)
shareViaProfile.value = true
}
)
val hasRelevantContacts = chatModel.chats.value.any { chat ->
val ci = chat.chatInfo
ci is ChatInfo.Direct &&
ci.contact.active &&
!ci.contact.isContactCard &&
!ci.contact.contactConnIncognito
}
if (hasRelevantContacts) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.share_address_with_contacts_question),
text = generalGetString(MR.strings.add_address_to_your_profile),
confirmText = generalGetString(MR.strings.share_verb),
onConfirm = {
setProfileAddress(true)
shareViaProfile.value = true
}
)
progressIndicator.value = false
} else {
setProfileAddress(true)
shareViaProfile.value = true
}
} else {
progressIndicator.value = false
}
progressIndicator.value = false
}
}
@@ -103,6 +121,7 @@ fun UserAddressView(
user = user.value,
userAddress = userAddress.value,
shareViaProfile,
onboarding = onboarding,
createAddress = ::createAddress,
showAddShortLinkAlert = { shareAddress: (() -> Unit)? ->
showAddShortLinkAlert(progressIndicator = progressIndicator, share = ::share, shareAddress = shareAddress)
@@ -249,6 +268,7 @@ private fun UserAddressLayout(
user: User?,
userAddress: UserContactLinkRec?,
shareViaProfile: MutableState<Boolean>,
onboarding: Boolean = false,
createAddress: () -> Unit,
showAddShortLinkAlert: ((() -> Unit)?) -> Unit,
learnMore: () -> Unit,
@@ -259,68 +279,100 @@ private fun UserAddressLayout(
saveAddressSettings: (AddressSettingsState, MutableState<AddressSettingsState>) -> Unit,
) {
ColumnWithScrollBar {
AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId))
if (!onboarding) {
AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId))
}
if (BuildConfigCommon.SIMPLEX_ASSETS && userAddress != null) {
Image(
painterResource(if (isInDarkTheme()) {
if (onboarding) MR.images.simplex_address_light else MR.images.simplex_address_small_light
} else {
if (onboarding) MR.images.simplex_address else MR.images.simplex_address_small
}),
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.fillMaxWidth()
)
}
Column(
Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
if (userAddress == null) {
SectionView(generalGetString(MR.strings.for_social_media).uppercase()) {
CreateAddressButton(createAddress)
}
if (!onboarding) {
SectionView(generalGetString(MR.strings.for_social_media).uppercase()) {
CreateAddressButton(createAddress)
}
SectionDividerSpaced()
SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) {
CreateOneTimeLinkButton()
}
SectionDividerSpaced()
SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) {
CreateOneTimeLinkButton()
}
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
SectionView {
LearnMoreButton(learnMore)
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
SectionView {
LearnMoreButton(learnMore)
}
}
} else {
val addressSettingsState = remember { mutableStateOf(AddressSettingsState(settings = userAddress.addressSettings)) }
val savedAddressSettingsState = remember { mutableStateOf(addressSettingsState.value) }
val showShortLink = remember { mutableStateOf(true) }
SectionViewWithButton(
stringResource(MR.strings.for_social_media).uppercase(),
titleButton = if (userAddress.connLinkContact.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null
) {
if (onboarding) {
Text(
stringResource(MR.strings.onboarding_post_address),
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF),
style = MaterialTheme.typography.body1
)
LinkTextView(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value), true)
Text(
stringResource(MR.strings.onboarding_or_use_qr_code),
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF),
style = MaterialTheme.typography.body1
)
SimpleXCreatedLinkQRCode(userAddress.connLinkContact, short = showShortLink.value)
if (userAddress.shouldBeUpgraded) {
AddShortLinkButton(text = stringResource(MR.strings.add_short_link)) { showAddShortLinkAlert(null) }
}
ShareAddressButton {
} else {
val addressSettingsState = remember { mutableStateOf(AddressSettingsState(settings = userAddress.addressSettings)) }
val savedAddressSettingsState = remember { mutableStateOf(addressSettingsState.value) }
SectionViewWithButton(
stringResource(MR.strings.for_social_media).uppercase(),
titleButton = if (userAddress.connLinkContact.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null
) {
SimpleXCreatedLinkQRCode(userAddress.connLinkContact, short = showShortLink.value)
if (userAddress.shouldBeUpgraded) {
showAddShortLinkAlert { share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) }
} else {
share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value))
AddShortLinkButton(text = stringResource(MR.strings.add_short_link)) { showAddShortLinkAlert(null) }
}
ShareAddressButton {
if (userAddress.shouldBeUpgraded) {
showAddShortLinkAlert { share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) }
} else {
share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value))
}
}
// ShareViaEmailButton { sendEmail(userAddress) }
BusinessAddressToggle(addressSettingsState) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) }
AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAddressSettings)
if (addressSettingsState.value.businessAddress) {
SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations))
}
}
// ShareViaEmailButton { sendEmail(userAddress) }
BusinessAddressToggle(addressSettingsState) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) }
AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAddressSettings)
if (addressSettingsState.value.businessAddress) {
SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations))
SectionDividerSpaced(maxTopPadding = addressSettingsState.value.businessAddress)
SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) {
CreateOneTimeLinkButton()
}
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
LearnMoreButton(learnMore)
}
}
SectionDividerSpaced(maxTopPadding = addressSettingsState.value.businessAddress)
SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) {
CreateOneTimeLinkButton()
}
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
LearnMoreButton(learnMore)
}
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
DeleteAddressButton(deleteAddress)
SectionTextFooter(stringResource(MR.strings.your_contacts_will_remain_connected))
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
DeleteAddressButton(deleteAddress)
SectionTextFooter(stringResource(MR.strings.your_contacts_will_remain_connected))
}
}
}
}
@@ -465,6 +465,15 @@
<string name="tap_to_start_new_chat">Tap to start a new chat</string>
<string name="chat_with_developers">Chat with the developers</string>
<string name="you_have_no_chats">You have no chats</string>
<string name="talk_to_someone">Talk to someone</string>
<string name="let_someone_connect_to_you">Let someone connect to you</string>
<string name="connect_via_link_or_qr_code">Connect via link or QR code</string>
<string name="connect_with_someone">Connect with someone</string>
<string name="invite_someone_privately">Invite someone privately</string>
<string name="a_link_for_one_person">A link for one person to connect</string>
<string name="create_your_public_address">Create your public address</string>
<string name="your_public_address">Your public address</string>
<string name="for_anyone_to_reach_you">For anyone to reach you</string>
<string name="loading_chats">Loading chats…</string>
<string name="no_filtered_chats">No filtered chats</string>
<string name="no_chats_in_list">No chats in list %s.</string>
@@ -904,7 +913,7 @@
<string name="new_chat">New chat</string>
<string name="new_message">New message</string>
<string name="add_contact_tab">Add contact</string>
<string name="scan_paste_link">Scan / Paste link</string>
<string name="scan_paste_link">Paste link / Scan</string>
<string name="paste_link">Paste link</string>
<string name="one_time_link">One-time invitation link</string>
<string name="one_time_link_short">1-time link</string>
@@ -1132,12 +1141,12 @@
<string name="all_your_contacts_will_remain_connected">All your contacts will remain connected.</string>
<string name="all_your_contacts_will_remain_connected_update_sent">All your contacts will remain connected. Profile update will be sent to your contacts.</string>
<string name="share_link">Share link</string>
<string name="add_address_to_your_profile">Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</string>
<string name="add_address_to_your_profile">Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts.</string>
<string name="create_address_and_let_people_connect">Create an address to let people connect with you.</string>
<string name="create_simplex_address">Create SimpleX address</string>
<string name="share_with_contacts">Share with contacts</string>
<string name="share_address_with_contacts_question">Share address with contacts?</string>
<string name="profile_update_will_be_sent_to_contacts">Profile update will be sent to your contacts.</string>
<string name="share_with_contacts">Share with SimpleX contacts</string>
<string name="share_address_with_contacts_question">Share address with SimpleX contacts?</string>
<string name="profile_update_will_be_sent_to_contacts">Profile update will be sent to your SimpleX contacts.</string>
<string name="stop_sharing_address">Stop sharing address?</string>
<string name="stop_sharing">Stop sharing</string>
<string name="auto_accept_contact">Auto-accept</string>
@@ -1154,6 +1163,11 @@
<string name="or_to_share_privately">Or to share privately</string>
<string name="simplex_address_or_1_time_link">SimpleX address or 1-time link?</string>
<string name="create_1_time_link">Create 1-time link</string>
<string name="new_1_time_link">New 1-time link</string>
<string name="onboarding_send_1_time_link">Send the link via any messenger - it\'s secure. Ask to paste into SimpleX.</string>
<string name="onboarding_or_show_qr_code">Or show QR in person or via video call.</string>
<string name="onboarding_post_address">Use this address in your social media profile, website, or email signature.</string>
<string name="onboarding_or_use_qr_code">Or use this QR - print or show online.</string>
<string name="address_settings">Address settings</string>
<string name="business_address">Business address</string>
<string name="add_your_team_members_to_conversations">Add your team members to the conversations.</string>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M85-704.5V-875h170.5v57.5h-113v113H85ZM85-85v-170.5h57.5v113h113V-85H85Zm619.5 0v-57.5h113v-113H875V-85H704.5Zm113-619.5v-113h-113V-875H875v170.5h-57.5ZM705-254h61.5v61.5H705V-254Zm0-123h61.5v61.5H705V-377Zm-61.5 61.5H705v61.5h-61.5v-61.5ZM582-254h61.5v61.5H582V-254Zm-61.5-61.5H582v61.5h-61.5v-61.5Zm123-123H705v61.5h-61.5v-61.5ZM582-377h61.5v61.5H582V-377Zm-61.5-61.5H582v61.5h-61.5v-61.5Zm246-329v246h-246v-246h246Zm-328 329v246h-246v-246h246Zm0-329v246h-246v-246h246Zm-48 526.5v-149.5H241V-241h149.5Zm0-328.5V-719H241v149.5h149.5Zm327.5 0V-719H568.5v149.5H718Z"/></svg>

After

Width:  |  Height:  |  Size: 689 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0,0h24v24H0z" fill="#00000000"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0,0h24v24H0z" fill="#00000000"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0,0h24v24H0z" fill="#00000000"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0,0h24v24H0z" fill="#00000000"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0,0h24v24H0z" fill="#00000000"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0,0h24v24H0z" fill="#00000000"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0,0h24v24H0z" fill="#00000000"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M0,0h24v24H0z" fill="#00000000"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<rect width="24" height="24" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 175 B

@@ -3,11 +3,12 @@ package chat.simplex.common
import chat.simplex.common.model.json
import chat.simplex.common.platform.appPreferences
import chat.simplex.common.platform.desktopPlatform
import chat.simplex.common.ui.theme.DEFAULT_WINDOW_WIDTH
import kotlinx.serialization.*
@Serializable
data class WindowPositionSize(
val width: Int = 1366,
val width: Int = DEFAULT_WINDOW_WIDTH.value.toInt(),
val height: Int = 768,
val x: Int = 0,
val y: Int = 0,