mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01: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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user