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>
@@ -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,
|
||||
|
||||