mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-14 21:15:37 +00:00
desktop: in-app update functionality (#4443)
* desktop: in-app update functionality * without Android * refactor * working windows * tabs vs spaces * better working mac * changes * repo * undo manifest changes * changes * changes * unneeded changes * revert * new line * fix update notice * different way * changes to mac logic * changes to mac logic * more * update strings --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
committed by
GitHub
parent
3e873fcb32
commit
670bf34ff5
@@ -105,6 +105,7 @@ kotlin {
|
||||
implementation("uk.co.caprica:vlcj:4.8.2")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
}
|
||||
}
|
||||
val desktopTest by getting
|
||||
|
||||
+2
@@ -29,6 +29,8 @@ actual val remoteHostsDir: File = File(tmpDir.absolutePath + File.separator + "r
|
||||
|
||||
actual fun desktopOpenDatabaseDir() {}
|
||||
|
||||
actual fun desktopOpenDir(dir: File) {}
|
||||
|
||||
@Composable
|
||||
actual fun rememberFileChooserLauncher(getContent: Boolean, rememberedValue: Any?, onResult: (URI?) -> Unit): FileChooserLauncher {
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
|
||||
@@ -80,6 +80,7 @@ fun MainScreen() {
|
||||
laUnavailableInstructionAlert()
|
||||
}
|
||||
}
|
||||
platform.desktopShowAppUpdateNotice()
|
||||
LaunchedEffect(chatModel.clearOverlays.value) {
|
||||
if (chatModel.clearOverlays.value) {
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
|
||||
@@ -28,6 +28,7 @@ import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.*
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -122,6 +123,9 @@ object ChatModel {
|
||||
val clipboardHasText = mutableStateOf(false)
|
||||
val networkInfo = mutableStateOf(UserNetworkInfo(networkType = UserNetworkType.OTHER, online = true))
|
||||
|
||||
val updatingProgress = mutableStateOf(null as Float?)
|
||||
var updatingRequest: Closeable? = null
|
||||
|
||||
val updatingChatsMutex: Mutex = Mutex()
|
||||
val changingActiveUserMutex: Mutex = Mutex()
|
||||
|
||||
|
||||
+6
@@ -160,6 +160,9 @@ class AppPreferences {
|
||||
val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true)
|
||||
val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true)
|
||||
val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null)
|
||||
val appUpdateChannel = mkEnumPreference(SHARED_PREFS_APP_UPDATE_CHANNEL, AppUpdatesChannel.DISABLED) { AppUpdatesChannel.entries.firstOrNull { it.name == this } }
|
||||
val appSkippedUpdate = mkStrPreference(SHARED_PREFS_APP_SKIPPED_UPDATE, "")
|
||||
val appUpdateNoticeShown = mkBoolPreference(SHARED_PREFS_APP_UPDATE_NOTICE_SHOWN, false)
|
||||
|
||||
val onboardingStage = mkEnumPreference(SHARED_PREFS_ONBOARDING_STAGE, OnboardingStage.OnboardingComplete) { OnboardingStage.values().firstOrNull { it.name == this } }
|
||||
val migrationToStage = mkStrPreference(SHARED_PREFS_MIGRATION_TO_STAGE, null)
|
||||
@@ -331,6 +334,9 @@ class AppPreferences {
|
||||
private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName"
|
||||
private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime"
|
||||
private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage"
|
||||
private const val SHARED_PREFS_APP_UPDATE_CHANNEL = "AppUpdateChannel"
|
||||
private const val SHARED_PREFS_APP_SKIPPED_UPDATE = "AppSkippedUpdate"
|
||||
private const val SHARED_PREFS_APP_UPDATE_NOTICE_SHOWN = "AppUpdateNoticeShown"
|
||||
private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage"
|
||||
const val SHARED_PREFS_MIGRATION_TO_STAGE = "MigrationToStage"
|
||||
const val SHARED_PREFS_MIGRATION_FROM_STAGE = "MigrationFromStage"
|
||||
|
||||
+15
@@ -3,6 +3,8 @@ package chat.simplex.common.platform
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.DefaultTheme
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
import java.util.*
|
||||
|
||||
enum class AppPlatform {
|
||||
@@ -56,3 +58,16 @@ fun runMigrations() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class AppUpdatesChannel {
|
||||
DISABLED,
|
||||
STABLE,
|
||||
BETA;
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
DISABLED -> generalGetString(MR.strings.app_check_for_updates_disabled)
|
||||
STABLE -> generalGetString(MR.strings.app_check_for_updates_stable)
|
||||
BETA -> generalGetString(MR.strings.app_check_for_updates_beta)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ expect val remoteHostsDir: File
|
||||
|
||||
expect fun desktopOpenDatabaseDir()
|
||||
|
||||
expect fun desktopOpenDir(dir: File)
|
||||
|
||||
fun createURIFromPath(absolutePath: String): URI = URI.create(URLEncoder.encode(absolutePath, "UTF-8"))
|
||||
|
||||
fun URI.toFile(): File = File(URLDecoder.decode(rawPath, "UTF-8").removePrefix("file:"))
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ interface PlatformInterface {
|
||||
@Composable fun desktopScrollBarComponents(): Triple<Animatable<Float, AnimationVector1D>, Modifier, MutableState<Job>> = remember { Triple(Animatable(0f), Modifier, mutableStateOf(Job())) }
|
||||
@Composable fun desktopScrollBar(state: LazyListState, modifier: Modifier, scrollBarAlpha: Animatable<Float, AnimationVector1D>, scrollJob: MutableState<Job>, reversed: Boolean) {}
|
||||
@Composable fun desktopScrollBar(state: ScrollState, modifier: Modifier, scrollBarAlpha: Animatable<Float, AnimationVector1D>, scrollJob: MutableState<Job>, reversed: Boolean) {}
|
||||
@Composable fun desktopShowAppUpdateNotice() {}
|
||||
}
|
||||
/**
|
||||
* Multiplatform project has separate directories per platform + common directory that contains directories per platform + common for all of them.
|
||||
|
||||
+31
-29
@@ -128,30 +128,6 @@ fun CIFileView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun progressIndicator() {
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(32.dp),
|
||||
color = if (isInDarkTheme()) FileDark else FileLight,
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun progressCircle(progress: Long, total: Long) {
|
||||
val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat()
|
||||
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
|
||||
val strokeColor = if (isInDarkTheme()) FileDark else FileLight
|
||||
Surface(
|
||||
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
||||
color = Color.Transparent,
|
||||
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Box(Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun fileIndicator() {
|
||||
Box(
|
||||
@@ -164,14 +140,14 @@ fun CIFileView(
|
||||
when (file.fileStatus) {
|
||||
is CIFileStatus.SndStored ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP -> progressIndicator()
|
||||
FileProtocol.XFTP -> CIFileViewScope.progressIndicator()
|
||||
FileProtocol.SMP -> fileIcon()
|
||||
FileProtocol.LOCAL -> fileIcon()
|
||||
}
|
||||
is CIFileStatus.SndTransfer ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
|
||||
FileProtocol.SMP -> progressIndicator()
|
||||
FileProtocol.XFTP -> CIFileViewScope.progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
|
||||
FileProtocol.SMP -> CIFileViewScope.progressIndicator()
|
||||
FileProtocol.LOCAL -> {}
|
||||
}
|
||||
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled))
|
||||
@@ -186,9 +162,9 @@ fun CIFileView(
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = painterResource(MR.images.ic_more_horiz))
|
||||
is CIFileStatus.RcvTransfer ->
|
||||
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
|
||||
progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
|
||||
CIFileViewScope.progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
|
||||
} else {
|
||||
progressIndicator()
|
||||
CIFileViewScope.progressIndicator()
|
||||
}
|
||||
is CIFileStatus.RcvAborted ->
|
||||
fileIcon(innerIcon = painterResource(MR.images.ic_sync_problem), color = MaterialTheme.colors.primary)
|
||||
@@ -265,6 +241,32 @@ fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher =
|
||||
}
|
||||
}
|
||||
|
||||
object CIFileViewScope {
|
||||
@Composable
|
||||
fun progressIndicator() {
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(32.dp),
|
||||
color = if (isInDarkTheme()) FileDark else FileLight,
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun progressCircle(progress: Long, total: Long) {
|
||||
val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat()
|
||||
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
|
||||
val strokeColor = if (isInDarkTheme()) FileDark else FileLight
|
||||
Surface(
|
||||
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
||||
color = Color.Transparent,
|
||||
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
|
||||
contentColor = LocalContentColor.current
|
||||
) {
|
||||
Box(Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
class ChatItemProvider: PreviewParameterProvider<ChatItem> {
|
||||
private val sentFile = ChatItem(
|
||||
|
||||
+22
-2
@@ -1,6 +1,8 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -30,6 +32,8 @@ import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
||||
import chat.simplex.common.views.usersettings.SettingsView
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.Call
|
||||
import chat.simplex.common.views.chat.group.ProgressIndicator
|
||||
import chat.simplex.common.views.chat.item.CIFileViewScope
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
@@ -187,8 +191,24 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean) {
|
||||
val serversSummary: MutableState<PresentedServersSummary?> = remember { mutableStateOf(null) }
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
|
||||
if (stopped) {
|
||||
val updatingProgress = remember { chatModel.updatingProgress }.value
|
||||
if (updatingProgress != null) {
|
||||
barButtons.add {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val hovered = interactionSource.collectIsHoveredAsState().value
|
||||
IconButton(onClick = {
|
||||
chatModel.updatingRequest?.close()
|
||||
}, Modifier.hoverable(interactionSource)) {
|
||||
if (hovered) {
|
||||
Icon(painterResource(MR.images.ic_close), null, tint = WarningOrange)
|
||||
} else if (updatingProgress == -1f) {
|
||||
CIFileViewScope.progressIndicator()
|
||||
} else {
|
||||
CIFileViewScope.progressCircle((updatingProgress * 100).toLong(), 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (stopped) {
|
||||
barButtons.add {
|
||||
IconButton(onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
|
||||
+18
-5
@@ -69,16 +69,19 @@ class AlertManager {
|
||||
fun showAlertDialogButtonsColumn(
|
||||
title: String,
|
||||
text: String? = null,
|
||||
textAlign: TextAlign = TextAlign.Center,
|
||||
dismissible: Boolean = true,
|
||||
onDismissRequest: (() -> Unit)? = null,
|
||||
hostDevice: Pair<Long?, String>? = null,
|
||||
belowTextContent: @Composable (() -> Unit) = {},
|
||||
buttons: @Composable () -> Unit,
|
||||
) {
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
|
||||
onDismissRequest = { onDismissRequest?.invoke(); if (dismissible) hideAlert() },
|
||||
title = alertTitle(title),
|
||||
buttons = {
|
||||
AlertContent(text, hostDevice, extraPadding = true) {
|
||||
AlertContent(text, hostDevice, extraPadding = true, textAlign = textAlign, belowTextContent = belowTextContent) {
|
||||
buttons()
|
||||
}
|
||||
},
|
||||
@@ -286,7 +289,14 @@ private fun alertTitle(title: String): (@Composable () -> Unit)? {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AlertContent(text: String?, hostDevice: Pair<Long?, String>?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) {
|
||||
private fun AlertContent(
|
||||
text: String?,
|
||||
hostDevice: Pair<Long?, String>?,
|
||||
extraPadding: Boolean = false,
|
||||
textAlign: TextAlign = TextAlign.Center,
|
||||
belowTextContent: @Composable (() -> Unit) = {},
|
||||
content: @Composable (() -> Unit)
|
||||
) {
|
||||
BoxWithConstraints {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -300,17 +310,20 @@ private fun AlertContent(text: String?, hostDevice: Pair<Long?, String>?, extraP
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
if (text != null) {
|
||||
Column(Modifier.heightIn(max = this@BoxWithConstraints.maxHeight * 0.7f)
|
||||
.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
escapedHtmlToAnnotatedString(text, LocalDensity.current),
|
||||
Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f),
|
||||
Modifier.fillMaxWidth(),
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
textAlign = textAlign,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
belowTextContent()
|
||||
Spacer(Modifier.height(DEFAULT_PADDING * 1.5f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-6
@@ -118,13 +118,8 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
featureDescription(painterResource(feature.icon), feature.titleId, feature.descrId, feature.link)
|
||||
}
|
||||
|
||||
val uriHandler = LocalUriHandler.current
|
||||
if (v.post != null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = DEFAULT_PADDING.div(4))) {
|
||||
Text(stringResource(MR.strings.whats_new_read_more), color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.clickable { uriHandler.openUriCatching(v.post) })
|
||||
Icon(painterResource(MR.images.ic_open_in_new), stringResource(MR.strings.whats_new_read_more), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
ReadMoreButton(v.post)
|
||||
}
|
||||
|
||||
if (!viaSettings) {
|
||||
@@ -149,6 +144,16 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReadMoreButton(url: String) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = DEFAULT_PADDING.div(4))) {
|
||||
Text(stringResource(MR.strings.whats_new_read_more), color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.clickable { uriHandler.openUriCatching(url) })
|
||||
Icon(painterResource(MR.images.ic_open_in_new), stringResource(MR.strings.whats_new_read_more), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
private data class FeatureDescription(
|
||||
val icon: ImageResource,
|
||||
val titleId: StringResource,
|
||||
|
||||
+3
@@ -73,6 +73,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt
|
||||
withAuth = ::doWithAuth,
|
||||
drawerState = drawerState,
|
||||
)
|
||||
KeyChangeEffect(chatModel.updatingProgress.value != null) {
|
||||
drawerState.close()
|
||||
}
|
||||
}
|
||||
|
||||
val simplexTeamUri =
|
||||
|
||||
@@ -776,6 +776,25 @@
|
||||
<string name="app_version_code">App build: %s</string>
|
||||
<string name="core_version">Core version: v%s</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="app_check_for_updates">Check for updates</string>
|
||||
<string name="app_check_for_updates_disabled">Disabled</string>
|
||||
<string name="app_check_for_updates_stable">Stable</string>
|
||||
<string name="app_check_for_updates_beta">Beta</string>
|
||||
<string name="app_check_for_updates_update_available">Update available: %s</string>
|
||||
<string name="app_check_for_updates_button_download">Download %s (%s)</string>
|
||||
<string name="app_check_for_updates_button_skip">Skip this version</string>
|
||||
<string name="app_check_for_updates_download_started">Downloading app update, don\'t close the app</string>
|
||||
<string name="app_check_for_updates_download_completed_title">App update is downloaded</string>
|
||||
<string name="app_check_for_updates_button_open">Open file location</string>
|
||||
<string name="app_check_for_updates_button_install">Install update</string>
|
||||
<string name="app_check_for_updates_installed_successfully_title">Installed successfully</string>
|
||||
<string name="app_check_for_updates_installed_successfully_desc">Please restart the app.</string>
|
||||
<string name="app_check_for_updates_canceled">Update download canceled</string>
|
||||
<string name="app_check_for_updates_button_remind_later">Remind later</string>
|
||||
<string name="app_check_for_updates_notice_title">Check for updates</string>
|
||||
<string name="app_check_for_updates_notice_desc">To be notified about the new releases, turn on periodic check for Stable or Beta versions.</string>
|
||||
<string name="app_check_for_updates_notice_disable">Disable</string>
|
||||
|
||||
<string name="show_dev_options">Show:</string>
|
||||
<string name="hide_dev_options">Hide:</string>
|
||||
<string name="show_developer_options">Show developer options</string>
|
||||
|
||||
+1
@@ -6,6 +6,7 @@ import chat.simplex.common.views.call.RcvCallInvitation
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import java.util.*
|
||||
import chat.simplex.res.MR
|
||||
import java.io.File
|
||||
|
||||
actual val appPlatform = AppPlatform.DESKTOP
|
||||
|
||||
|
||||
+5
-1
@@ -26,9 +26,13 @@ actual val databaseExportDir: File = tmpDir
|
||||
actual val remoteHostsDir: File = File(dataDir.absolutePath + File.separator + "remote_hosts")
|
||||
|
||||
actual fun desktopOpenDatabaseDir() {
|
||||
desktopOpenDir(dataDir)
|
||||
}
|
||||
|
||||
actual fun desktopOpenDir(dir: File) {
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(dataDir);
|
||||
Desktop.getDesktop().open(dir);
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
AlertManager.shared.showAlertMsg(
|
||||
|
||||
+6
-6
@@ -8,12 +8,12 @@ private val unixConfigPath = (System.getenv("XDG_CONFIG_HOME") ?: "$home/.config
|
||||
private val unixDataPath = (System.getenv("XDG_DATA_HOME") ?: "$home/.local/share") + "/simplex"
|
||||
val desktopPlatform = detectDesktopPlatform()
|
||||
|
||||
enum class DesktopPlatform(val libExtension: String, val configPath: String, val dataPath: String) {
|
||||
LINUX_X86_64("so", unixConfigPath, unixDataPath),
|
||||
LINUX_AARCH64("so", unixConfigPath, unixDataPath),
|
||||
WINDOWS_X86_64("dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX"),
|
||||
MAC_X86_64("dylib", unixConfigPath, unixDataPath),
|
||||
MAC_AARCH64("dylib", unixConfigPath, unixDataPath);
|
||||
enum class DesktopPlatform(val libExtension: String, val configPath: String, val dataPath: String, val githubAssetName: String) {
|
||||
LINUX_X86_64("so", unixConfigPath, unixDataPath, "simplex-desktop-x86_64.AppImage"),
|
||||
LINUX_AARCH64("so", unixConfigPath, unixDataPath, " simplex-desktop-aarch64.AppImage"),
|
||||
WINDOWS_X86_64("dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX", "simplex-desktop-windows-x86_64.msi"),
|
||||
MAC_X86_64("dylib", unixConfigPath, unixDataPath, "simplex-desktop-macos-x86_64.dmg"),
|
||||
MAC_AARCH64("dylib", unixConfigPath, unixDataPath, "simplex-desktop-macos-aarch64.dmg");
|
||||
|
||||
fun isLinux() = this == LINUX_X86_64 || this == LINUX_AARCH64
|
||||
fun isWindows() = this == WINDOWS_X86_64
|
||||
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.json
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.WarningOrange
|
||||
import chat.simplex.common.views.onboarding.ReadMoreButton
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
|
||||
@Serializable
|
||||
data class GitHubRelease(
|
||||
@SerialName("tag_name")
|
||||
val tagName: String,
|
||||
@SerialName("html_url")
|
||||
val htmlUrl: String,
|
||||
val name: String,
|
||||
val draft: Boolean,
|
||||
val prerelease: Boolean,
|
||||
val body: String,
|
||||
@SerialName("published_at")
|
||||
val publishedAt: String,
|
||||
val assets: List<GitHubAsset>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GitHubAsset(
|
||||
@SerialName("browser_download_url")
|
||||
val browserDownloadUrl: String,
|
||||
val name: String,
|
||||
val size: Long,
|
||||
|
||||
val isAppImage: Boolean = name.lowercase().contains(".appimage")
|
||||
)
|
||||
|
||||
fun showAppUpdateNotice() {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
generalGetString(MR.strings.app_check_for_updates_notice_title),
|
||||
text = generalGetString(MR.strings.app_check_for_updates_notice_desc),
|
||||
buttons = {
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
appPrefs.appUpdateChannel.set(AppUpdatesChannel.STABLE)
|
||||
setupUpdateChecker()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.app_check_for_updates_stable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
appPrefs.appUpdateChannel.set(AppUpdatesChannel.BETA)
|
||||
setupUpdateChecker()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.app_check_for_updates_beta), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
appPrefs.appUpdateChannel.set(AppUpdatesChannel.DISABLED)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.app_check_for_updates_notice_disable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var updateCheckerJob: Job = Job()
|
||||
fun setupUpdateChecker() = withLongRunningApi {
|
||||
updateCheckerJob.cancel()
|
||||
if (appPrefs.appUpdateChannel.get() == AppUpdatesChannel.DISABLED) {
|
||||
return@withLongRunningApi
|
||||
}
|
||||
checkForUpdate()
|
||||
createUpdateJob()
|
||||
}
|
||||
|
||||
private fun createUpdateJob() {
|
||||
updateCheckerJob = withLongRunningApi {
|
||||
delay(24 * 60 * 60 * 1000)
|
||||
checkForUpdate()
|
||||
createUpdateJob()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun checkForUpdate() {
|
||||
Log.d(TAG, "Checking for update")
|
||||
val client = setupHttpClient()
|
||||
try {
|
||||
val request = Request.Builder().url("https://api.github.com/repos/simplex-chat/simplex-chat/releases").addHeader("User-agent", "curl").build()
|
||||
client.newCall(request).execute().use { response ->
|
||||
response.body?.use {
|
||||
val body = it.string()
|
||||
val releases = json.decodeFromString<List<GitHubRelease>>(body).filterNot { it.draft }
|
||||
val release = when (appPrefs.appUpdateChannel.get()) {
|
||||
AppUpdatesChannel.STABLE -> releases.firstOrNull { !it.prerelease }
|
||||
AppUpdatesChannel.BETA -> releases.firstOrNull()
|
||||
AppUpdatesChannel.DISABLED -> return
|
||||
} ?: return
|
||||
val currentVersionName = "v" + (if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME)
|
||||
val redactedCurrentVersionName = when {
|
||||
currentVersionName.contains('-') && currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName.substringBefore('-')}.0-${currentVersionName.substringAfter('-')}"
|
||||
currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName}.0"
|
||||
else -> currentVersionName
|
||||
}
|
||||
if (release.tagName == appPrefs.appSkippedUpdate.get() || release.tagName == currentVersionName || release.tagName == redactedCurrentVersionName) {
|
||||
Log.d(TAG, "Skipping update because of the same version or skipped version")
|
||||
return
|
||||
}
|
||||
val assets = chooseGitHubReleaseAssets(release)
|
||||
// No need to show an alert if no suitable packages were found. But for Flatpak users it's useful to see release notes anyway
|
||||
if (assets.isEmpty() && !isRunningFromFlatpak()) {
|
||||
Log.d(TAG, "No assets to download for current system")
|
||||
return
|
||||
}
|
||||
val lines = ArrayList<String>()
|
||||
for (line in release.body.lines()) {
|
||||
if (line == "Commits:") break
|
||||
lines.add(line)
|
||||
}
|
||||
val text = lines.joinToString("\n")
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
generalGetString(MR.strings.app_check_for_updates_update_available).format(release.name),
|
||||
text = text,
|
||||
textAlign = TextAlign.Start,
|
||||
dismissible = false,
|
||||
belowTextContent = {
|
||||
ReadMoreButton(release.htmlUrl)
|
||||
},
|
||||
buttons = {
|
||||
Column {
|
||||
for (asset in assets) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
chatModel.updatingProgress.value = 0f
|
||||
withLongRunningApi {
|
||||
try {
|
||||
downloadAsset(asset)
|
||||
} finally {
|
||||
chatModel.updatingProgress.value = null
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(
|
||||
generalGetString(MR.strings.app_check_for_updates_button_download).format(
|
||||
if (asset.name.length > 34) "…" + asset.name.substringAfter("simplex-desktop-") else asset.name,
|
||||
formatBytes(asset.size)), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
skipRelease(release)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.app_check_for_updates_button_skip), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = WarningOrange)
|
||||
}
|
||||
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.app_check_for_updates_button_remind_later), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to get the latest release: ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupHttpClient(): OkHttpClient {
|
||||
val netCfg = getNetCfg()
|
||||
var proxy: Proxy? = null
|
||||
if (netCfg.useSocksProxy && netCfg.socksProxy != null) {
|
||||
val hostname = netCfg.socksProxy.substringBefore(":").ifEmpty { "localhost" }
|
||||
val port = netCfg.socksProxy.substringAfter(":").toIntOrNull()
|
||||
if (port != null) {
|
||||
proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(hostname, port))
|
||||
}
|
||||
}
|
||||
return OkHttpClient.Builder().proxy(proxy).followRedirects(true).build()
|
||||
}
|
||||
|
||||
private fun skipRelease(release: GitHubRelease) {
|
||||
appPrefs.appSkippedUpdate.set(release.tagName)
|
||||
}
|
||||
|
||||
private suspend fun downloadAsset(asset: GitHubAsset) {
|
||||
withContext(Dispatchers.Main) {
|
||||
showToast(generalGetString(MR.strings.app_check_for_updates_download_started))
|
||||
}
|
||||
val progressListener = object: ProgressListener {
|
||||
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
|
||||
if (contentLength != -1L) {
|
||||
chatModel.updatingProgress.value = if (done) 1f else bytesRead / contentLength.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
val client = setupHttpClient().newBuilder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
val body = originalResponse.body
|
||||
if (body != null) {
|
||||
originalResponse.newBuilder().body(ProgressResponseBody(body, progressListener)).build()
|
||||
} else {
|
||||
originalResponse
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
try {
|
||||
val request = Request.Builder().url(asset.browserDownloadUrl).addHeader("User-agent", "curl").build()
|
||||
val call = client.newCall(request)
|
||||
chatModel.updatingRequest = Closeable {
|
||||
call.cancel()
|
||||
withApi {
|
||||
showToast(generalGetString(MR.strings.app_check_for_updates_canceled))
|
||||
}
|
||||
}
|
||||
call.execute().use { response ->
|
||||
response.body?.use { body ->
|
||||
body.byteStream().use { stream ->
|
||||
createTmpFileAndDelete { file ->
|
||||
// It's important to close output stream (with use{}), otherwise, Windows cannot rename the file
|
||||
file.outputStream().use { output ->
|
||||
stream.copyTo(output)
|
||||
}
|
||||
val newFile = File(file.parentFile, asset.name)
|
||||
file.renameTo(newFile)
|
||||
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
generalGetString(MR.strings.app_check_for_updates_download_completed_title),
|
||||
dismissible = false,
|
||||
buttons = {
|
||||
Column {
|
||||
// It's problematic to install .deb package because it requires either root or GUI package installer which is not available on
|
||||
// Debian by default. Let the user install it manually only
|
||||
if (!asset.name.lowercase().endsWith(".deb")) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
chatModel.updatingProgress.value = -1f
|
||||
withLongRunningApi {
|
||||
try {
|
||||
installAppUpdate(newFile)
|
||||
} finally {
|
||||
chatModel.updatingProgress.value = null
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.app_check_for_updates_button_install), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
SectionItemView({
|
||||
desktopOpenDir(newFile.parentFile)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.app_check_for_updates_button_open), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
newFile.delete()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to download the asset from release: ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isRunningFromFlatpak(): Boolean = System.getenv("container") == "flatpak"
|
||||
|
||||
private fun chooseGitHubReleaseAssets(release: GitHubRelease): List<GitHubAsset> {
|
||||
val res = if (isRunningFromFlatpak()) {
|
||||
// No need to show download options for Flatpak users
|
||||
emptyList()
|
||||
} else if (Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
|
||||
// Show all available .deb packages and user will choose the one that works on his system (for Debian derivatives)
|
||||
release.assets.filter { it.name.lowercase().endsWith(".deb") }
|
||||
} else {
|
||||
release.assets.filter { it.name == desktopPlatform.githubAssetName }
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
private suspend fun installAppUpdate(file: File) = withContext(Dispatchers.IO) {
|
||||
when {
|
||||
desktopPlatform.isLinux() -> {
|
||||
val process = Runtime.getRuntime().exec("xdg-open ${file.absolutePath}").onExit().join()
|
||||
val startedInstallation = process.exitValue() == 0 && process.children().count() > 0
|
||||
if (!startedInstallation) {
|
||||
Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}")
|
||||
// Failed to start installation. show directory with the file for manual installation
|
||||
desktopOpenDir(file.parentFile)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title),
|
||||
text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc)
|
||||
)
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
desktopPlatform.isWindows() -> {
|
||||
val process = Runtime.getRuntime().exec("msiexec /i ${file.absolutePath}"/* /qb */).onExit().join()
|
||||
val startedInstallation = process.exitValue() == 0
|
||||
if (!startedInstallation) {
|
||||
Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}")
|
||||
// Failed to start installation. show directory with the file for manual installation
|
||||
desktopOpenDir(file.parentFile)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title),
|
||||
text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc)
|
||||
)
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
desktopPlatform.isMac() -> {
|
||||
// Default mount point if no other DMGs were mounted before
|
||||
var volume = "/Volumes/SimpleX"
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("hdiutil mount ${file.absolutePath}").onExit().join()
|
||||
val startedInstallation = process.exitValue() == 0
|
||||
val lines = process.inputReader().use { it.readLines() }
|
||||
// This is needed for situations when mount point has non-default path.
|
||||
// For example, when a user already had mounted SimpleX.dmg before and default mount point is not available.
|
||||
// Mac will make volume like /Volumes/SimpleX 1
|
||||
val lastLine = lines.lastOrNull()?.substringAfterLast('\t')
|
||||
if (!startedInstallation || lastLine == null || !lastLine.lowercase().contains("/volumes/")) {
|
||||
Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}")
|
||||
// Failed to start installation. show directory with the file for manual installation
|
||||
desktopOpenDir(file.parentFile)
|
||||
return@withContext
|
||||
}
|
||||
volume = lastLine
|
||||
File("/Applications/SimpleX.app").renameTo(File("/Applications/SimpleX-old.app"))
|
||||
val process2 = Runtime.getRuntime().exec(arrayOf("cp", "-R", "${volume}/SimpleX.app", "/Applications")).onExit().join()
|
||||
val copiedSuccessfully = process2.exitValue() == 0
|
||||
if (!copiedSuccessfully) {
|
||||
Log.e(TAG, "Error copying the app: ${process2.inputReader().use { it.readLines().joinToString("\n") }}${process2.errorStream.use { String(it.readAllBytes()) }}")
|
||||
// Failed to start installation. show directory with the file for manual installation
|
||||
desktopOpenDir(file.parentFile)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title),
|
||||
text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc)
|
||||
)
|
||||
file.delete()
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
Runtime.getRuntime().exec(arrayOf("hdiutil", "unmount", volume)).onExit().join()
|
||||
} finally {
|
||||
if (!File("/Applications/SimpleX.app").exists()) {
|
||||
File("/Applications/SimpleX-old.app").renameTo(File("/Applications/SimpleX.app"))
|
||||
} else {
|
||||
Runtime.getRuntime().exec("rm -rf /Applications/SimpleX-old.app").onExit().join()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.ResponseBody
|
||||
import okio.*
|
||||
|
||||
// https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java
|
||||
class ProgressResponseBody(
|
||||
val responseBody: ResponseBody,
|
||||
val progressListener: ProgressListener
|
||||
): ResponseBody() {
|
||||
private var bufferedSource: BufferedSource? = null
|
||||
|
||||
override fun contentType(): MediaType? {
|
||||
return responseBody.contentType()
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
return responseBody.contentLength()
|
||||
}
|
||||
|
||||
override fun source(): BufferedSource {
|
||||
if (bufferedSource == null) {
|
||||
bufferedSource = source(responseBody.source()).buffer()
|
||||
}
|
||||
return bufferedSource!!
|
||||
}
|
||||
|
||||
private fun source(source: Source): Source {
|
||||
return object: ForwardingSource(source) {
|
||||
var totalBytesRead = 0L
|
||||
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
val bytesRead = super.read(sink, byteCount)
|
||||
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||
totalBytesRead += if (bytesRead != -1L) bytesRead else 0L
|
||||
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||
return bytesRead;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ProgressListener {
|
||||
fun update(bytesRead: Long, contentLength: Long, done: Boolean);
|
||||
}
|
||||
+16
-1
@@ -1,9 +1,16 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.views.helpers.ModalData
|
||||
import chat.simplex.common.platform.AppUpdatesChannel
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -17,6 +24,14 @@ actual fun SettingsSectionApp(
|
||||
) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_app)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }, extraPadding = true)
|
||||
val selectedChannel = remember { appPrefs.appUpdateChannel.state }
|
||||
val values = AppUpdatesChannel.entries.map { it to it.text }
|
||||
Box(Modifier.padding(start = DEFAULT_PADDING_HALF * 1.4f)) {
|
||||
ExposedDropDownSettingRow(stringResource(MR.strings.app_check_for_updates), values, selectedChannel) {
|
||||
appPrefs.appUpdateChannel.set(it)
|
||||
setupUpdateChecker()
|
||||
}
|
||||
}
|
||||
AppVersionItem(showVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.platform.DesktopPlatform
|
||||
import chat.simplex.common.showApp
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
|
||||
@@ -20,6 +22,7 @@ fun main() {
|
||||
//System.setProperty("skiko.renderApi", "SOFTWARE")
|
||||
initHaskell()
|
||||
runMigrations()
|
||||
setupUpdateChecker()
|
||||
initApp()
|
||||
tmpDir.deleteRecursively()
|
||||
tmpDir.mkdir()
|
||||
@@ -75,6 +78,31 @@ private fun initHaskell() {
|
||||
override fun desktopScrollBar(state: ScrollState, modifier: Modifier, scrollBarAlpha: Animatable<Float, AnimationVector1D>, scrollJob: MutableState<Job>, reversed: Boolean) {
|
||||
DesktopScrollBar(rememberScrollbarAdapter(scrollState = state), modifier, scrollBarAlpha, scrollJob, reversed)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun desktopShowAppUpdateNotice() {
|
||||
fun showNoticeIfNeeded() {
|
||||
if (
|
||||
!chatModel.controller.appPrefs.appUpdateNoticeShown.get()
|
||||
&& chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete
|
||||
&& chatModel.chats.size > 3
|
||||
&& chatModel.activeCallInvitation.value == null
|
||||
) {
|
||||
appPrefs.appUpdateNoticeShown.set(true)
|
||||
showAppUpdateNotice()
|
||||
}
|
||||
}
|
||||
// Will show notice if chats were loaded before that moment and number of chats > 3
|
||||
LaunchedEffect(Unit) {
|
||||
showNoticeIfNeeded()
|
||||
}
|
||||
// Will show notice if chats were loaded later (a lot of chats/slow query) and number of chats > 3
|
||||
KeyChangeEffect(chatModel.chats.size) { oldSize ->
|
||||
if (oldSize == 0) {
|
||||
showNoticeIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user