mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d62d7ddfb | |||
| b58ccedf23 | |||
| a78aeb47c5 | |||
| 01cadefde7 | |||
| c70e7223d9 | |||
| 94df5d47d1 | |||
| f9cba3f8e0 | |||
| 3655971aec | |||
| dea45e3228 | |||
| 4f2102ac6e | |||
| 17bcdaf6c4 | |||
| 1a2fad070e | |||
| 6101be5657 | |||
| a10dd81a1d | |||
| 289130832a | |||
| 224cfe9e02 | |||
| 0679d023e2 | |||
| 6d5869e974 | |||
| f0974af03a | |||
| da70aa30b6 |
@@ -37,7 +37,7 @@ kotlin {
|
||||
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
||||
api("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
|
||||
api("com.russhwolf:multiplatform-settings:1.1.1")
|
||||
api("com.charleskorn.kaml:kaml:0.58.0")
|
||||
api("com.charleskorn.kaml:kaml:0.59.0")
|
||||
api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}")
|
||||
implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}")
|
||||
//Barcode
|
||||
|
||||
+2
@@ -4104,6 +4104,8 @@ val jsonShort = Json {
|
||||
val yaml = Yaml(configuration = YamlConfiguration(
|
||||
strictMode = false,
|
||||
encodeDefaults = false,
|
||||
/** ~5.5 MB limitation since wallpaper is limited by 5 MB, see [saveWallpaperFile] */
|
||||
codePointLimit = 5500000,
|
||||
))
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -506,6 +506,76 @@ data class ThemeModeOverride (
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun removeSameColors(base: DefaultTheme): ThemeModeOverride {
|
||||
val c = when (base) {
|
||||
DefaultTheme.LIGHT -> LightColorPalette
|
||||
DefaultTheme.DARK -> DarkColorPalette
|
||||
DefaultTheme.SIMPLEX -> SimplexColorPalette
|
||||
DefaultTheme.BLACK -> BlackColorPalette
|
||||
}
|
||||
val ac = when (base) {
|
||||
DefaultTheme.LIGHT -> LightColorPaletteApp
|
||||
DefaultTheme.DARK -> DarkColorPaletteApp
|
||||
DefaultTheme.SIMPLEX -> SimplexColorPaletteApp
|
||||
DefaultTheme.BLACK -> BlackColorPaletteApp
|
||||
}
|
||||
val w = when (val wallpaperType = WallpaperType.from(wallpaper)) {
|
||||
is WallpaperType.Preset -> {
|
||||
val p = PresetWallpaper.from(wallpaperType.filename)
|
||||
ThemeWallpaper(
|
||||
preset = wallpaperType.filename,
|
||||
scale = p?.scale ?: wallpaper?.scale,
|
||||
scaleType = null,
|
||||
background = p?.background?.get(base)?.toReadableHex(),
|
||||
tint = p?.tint?.get(base)?.toReadableHex(),
|
||||
image = null,
|
||||
imageFile = null,
|
||||
)
|
||||
}
|
||||
is WallpaperType.Image -> {
|
||||
ThemeWallpaper(
|
||||
preset = null,
|
||||
scale = null,
|
||||
scaleType = WallpaperScaleType.FILL,
|
||||
background = Color.Transparent.toReadableHex(),
|
||||
tint = Color.Transparent.toReadableHex(),
|
||||
image = null,
|
||||
imageFile = null,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
ThemeWallpaper()
|
||||
}
|
||||
}
|
||||
|
||||
return copy(
|
||||
colors = ThemeColors(
|
||||
primary = if (colors.primary?.colorFromReadableHex() != c.primary) colors.primary else null,
|
||||
primaryVariant = if (colors.primaryVariant?.colorFromReadableHex() != c.primaryVariant) colors.primaryVariant else null,
|
||||
secondary = if (colors.secondary?.colorFromReadableHex() != c.secondary) colors.secondary else null,
|
||||
secondaryVariant = if (colors.secondaryVariant?.colorFromReadableHex() != c.secondaryVariant) colors.secondaryVariant else null,
|
||||
background = if (colors.background?.colorFromReadableHex() != c.background) colors.background else null,
|
||||
surface = if (colors.surface?.colorFromReadableHex() != c.surface) colors.surface else null,
|
||||
title = if (colors.title?.colorFromReadableHex() != ac.title) colors.title else null,
|
||||
primaryVariant2 = if (colors.primaryVariant2?.colorFromReadableHex() != ac.primaryVariant2) colors.primary else null,
|
||||
sentMessage = if (colors.sentMessage?.colorFromReadableHex() != ac.sentMessage) colors.sentMessage else null,
|
||||
sentQuote = if (colors.sentQuote?.colorFromReadableHex() != ac.sentQuote) colors.sentQuote else null,
|
||||
receivedMessage = if (colors.receivedMessage?.colorFromReadableHex() != ac.receivedMessage) colors.receivedMessage else null,
|
||||
receivedQuote = if (colors.receivedQuote?.colorFromReadableHex() != ac.receivedQuote) colors.receivedQuote else null,
|
||||
),
|
||||
wallpaper = wallpaper?.copy(
|
||||
preset = wallpaper.preset,
|
||||
scale = if (wallpaper.scale != w.scale) wallpaper.scale else null,
|
||||
scaleType = if (wallpaper.scaleType != w.scaleType) wallpaper.scaleType else null,
|
||||
background = if (wallpaper.background != w.background) wallpaper.background else null,
|
||||
tint = if (wallpaper.tint != w.tint) wallpaper.tint else null,
|
||||
image = wallpaper.image,
|
||||
imageFile = wallpaper.imageFile,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun withFilledAppDefaults(mode: DefaultThemeMode, base: DefaultTheme): ThemeModeOverride =
|
||||
ThemeModeOverride(
|
||||
|
||||
+24
-2
@@ -42,6 +42,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun ChatInfoView(
|
||||
@@ -719,7 +720,7 @@ fun ModalData.ChatWallpaperEditorModal(chat: Chat) {
|
||||
|
||||
suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, chat: Chat) {
|
||||
val unchangedThemes: ThemeModeOverrides = ((chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes ?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes) ?: ThemeModeOverrides()
|
||||
val wallpaperFiles = listOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile)
|
||||
val wallpaperFiles = setOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile)
|
||||
var changedThemes: ThemeModeOverrides? = unchangedThemes
|
||||
val changed = newTheme?.copy(wallpaper = newTheme.wallpaper?.withFilledWallpaperPath())
|
||||
changedThemes = when (applyToMode) {
|
||||
@@ -727,7 +728,28 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c
|
||||
DefaultThemeMode.LIGHT -> changedThemes?.copy(light = changed?.copy(mode = applyToMode))
|
||||
DefaultThemeMode.DARK -> changedThemes?.copy(dark = changed?.copy(mode = applyToMode))
|
||||
}
|
||||
changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) changedThemes else null
|
||||
changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) {
|
||||
val light = changedThemes.light
|
||||
val dark = changedThemes.dark
|
||||
val currentMode = CurrentColors.value.base.mode
|
||||
// same image file for both modes, copy image to make them as different files
|
||||
if (light?.wallpaper?.imageFile != null && dark?.wallpaper?.imageFile != null && light.wallpaper.imageFile == dark.wallpaper.imageFile) {
|
||||
val imageFile = if (currentMode == DefaultThemeMode.LIGHT) {
|
||||
dark.wallpaper.imageFile
|
||||
} else {
|
||||
light.wallpaper.imageFile
|
||||
}
|
||||
val filePath = saveWallpaperFile(File(getWallpaperFilePath(imageFile)).toURI())
|
||||
changedThemes = if (currentMode == DefaultThemeMode.LIGHT) {
|
||||
changedThemes.copy(dark = dark.copy(wallpaper = dark.wallpaper.copy(imageFile = filePath)))
|
||||
} else {
|
||||
changedThemes.copy(light = light.copy(wallpaper = light.wallpaper.copy(imageFile = filePath)))
|
||||
}
|
||||
}
|
||||
changedThemes
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val wallpaperFilesToDelete = wallpaperFiles - changedThemes?.light?.wallpaper?.imageFile - changedThemes?.dark?.wallpaper?.imageFile
|
||||
wallpaperFilesToDelete.forEach(::removeWallpaperFile)
|
||||
|
||||
|
||||
+10
-2
@@ -339,6 +339,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
openDirectChat(chatRh, contactId, chatModel)
|
||||
}
|
||||
},
|
||||
forwardItem = { cItem, cInfo ->
|
||||
chatModel.chatId.value = null
|
||||
chatModel.sharedContent.value = SharedContent.Forward(cInfo, cItem)
|
||||
},
|
||||
updateContactStats = { contact ->
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
@@ -537,6 +541,7 @@ fun ChatLayout(
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
forwardItem: (ChatInfo, ChatItem) -> Unit,
|
||||
updateContactStats: (Contact) -> Unit,
|
||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||
syncContactConnection: (Contact) -> Unit,
|
||||
@@ -619,7 +624,7 @@ fun ChatLayout(
|
||||
ChatItemsList(
|
||||
chat, unreadCount, composeState, searchValue,
|
||||
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
|
||||
)
|
||||
@@ -888,6 +893,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
forwardItem: (ChatInfo, ChatItem) -> Unit,
|
||||
updateContactStats: (Contact) -> Unit,
|
||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||
syncContactConnection: (Contact) -> Unit,
|
||||
@@ -991,7 +997,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
tryOrShowError("${cItem.id}ChatItem", error = {
|
||||
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
|
||||
}) {
|
||||
ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
|
||||
ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1544,6 +1550,7 @@ fun PreviewChatLayout() {
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
forwardItem = { _, _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
@@ -1617,6 +1624,7 @@ fun PreviewGroupChatLayout() {
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
forwardItem = { _, _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
|
||||
+10
-5
@@ -59,6 +59,7 @@ fun ChatItemView(
|
||||
scrollToItem: (Long) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
forwardItem: (ChatInfo, ChatItem) -> Unit,
|
||||
updateContactStats: (Contact) -> Unit,
|
||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||
syncContactConnection: (Contact) -> Unit,
|
||||
@@ -68,7 +69,8 @@ fun ChatItemView(
|
||||
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
|
||||
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean
|
||||
showViaProxy: Boolean,
|
||||
preview: Boolean = false,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val sent = cItem.chatDir.sent
|
||||
@@ -260,8 +262,7 @@ fun ChatItemView(
|
||||
!cItem.isLiveDummy && !live
|
||||
) {
|
||||
ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = {
|
||||
chatModel.chatId.value = null
|
||||
chatModel.sharedContent.value = SharedContent.Forward(cItem, cInfo)
|
||||
forwardItem(cInfo, cItem)
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
@@ -272,7 +273,7 @@ fun ChatItemView(
|
||||
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null && !cItem.localNote) {
|
||||
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
|
||||
}
|
||||
if (!(live && cItem.meta.isLive)) {
|
||||
if (!(live && cItem.meta.isLive) && !preview) {
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
}
|
||||
val groupInfo = cItem.memberToModerate(cInfo)?.first
|
||||
@@ -840,6 +841,7 @@ fun PreviewChatItemView(
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
forwardItem = { _, _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
@@ -850,6 +852,7 @@ fun PreviewChatItemView(
|
||||
showItemDetails = { _, _ -> },
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
preview = true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -875,6 +878,7 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
forwardItem = { _, _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
@@ -884,7 +888,8 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
setReaction = { _, _, _, _ -> },
|
||||
showItemDetails = { _, _ -> },
|
||||
developerTools = false,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
preview = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -379,6 +379,9 @@ fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, b
|
||||
val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
|
||||
val scaledWidth = (image.width * scale.scaleX).roundToInt()
|
||||
val scaledHeight = (image.height * scale.scaleY).roundToInt()
|
||||
// Large image will cause freeze
|
||||
if (image.width > 4320 || image.height > 4320) return@clipRect
|
||||
|
||||
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||
if (scaleType == WallpaperScaleType.FIT) {
|
||||
if (scaledWidth < size.width) {
|
||||
|
||||
+62
-5
@@ -3,13 +3,16 @@ package chat.simplex.common.views.helpers
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.yaml
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -18,6 +21,7 @@ import chat.simplex.common.views.usersettings.AppearanceScope.editColor
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
@@ -134,7 +138,7 @@ fun ModalData.UserWallpaperEditor(
|
||||
SectionSpacer()
|
||||
|
||||
if (!globalThemeUsed.value) {
|
||||
ResetToGlobalThemeButton {
|
||||
ResetToGlobalThemeButton(true) {
|
||||
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
globalThemeUsed.value = true
|
||||
withBGApi { save(applyToMode.value, null) }
|
||||
@@ -202,6 +206,14 @@ fun ModalData.UserWallpaperEditor(
|
||||
SectionSpacer()
|
||||
|
||||
AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor)
|
||||
|
||||
SectionSpacer()
|
||||
ImportExportThemeSection(null, remember { chatModel.currentUser }.value?.uiThemes) {
|
||||
withBGApi {
|
||||
themeModeOverride.value = it
|
||||
save(applyToMode.value, it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AdvancedSettingsButton { showMore = true }
|
||||
}
|
||||
@@ -226,7 +238,7 @@ fun ModalData.ChatWallpaperEditor(
|
||||
val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } }
|
||||
val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) {
|
||||
mutableStateOf(
|
||||
ThemeManager.currentColors(null, if (themeModeOverride.value == ThemeModeOverride()) null else themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
|
||||
ThemeManager.currentColors(null, if (globalThemeUsed.value) null else themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -363,7 +375,7 @@ fun ModalData.ChatWallpaperEditor(
|
||||
SectionSpacer()
|
||||
|
||||
if (!globalThemeUsed.value) {
|
||||
ResetToGlobalThemeButton {
|
||||
ResetToGlobalThemeButton(remember { chatModel.currentUser }.value?.uiThemes?.preferredMode(isInDarkTheme()) == null) {
|
||||
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
globalThemeUsed.value = true
|
||||
withBGApi { save(applyToMode.value, null) }
|
||||
@@ -431,6 +443,14 @@ fun ModalData.ChatWallpaperEditor(
|
||||
SectionSpacer()
|
||||
|
||||
AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor)
|
||||
|
||||
SectionSpacer()
|
||||
ImportExportThemeSection(themeModeOverride.value, remember { chatModel.currentUser }.value?.uiThemes) {
|
||||
withBGApi {
|
||||
themeModeOverride.value = it
|
||||
save(applyToMode.value, it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AdvancedSettingsButton { showMore = true }
|
||||
}
|
||||
@@ -440,9 +460,46 @@ fun ModalData.ChatWallpaperEditor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResetToGlobalThemeButton(onClick: () -> Unit) {
|
||||
private fun ImportExportThemeSection(perChat: ThemeModeOverride?, perUser: ThemeModeOverrides?, save: (ThemeModeOverride) -> Unit) {
|
||||
SectionView {
|
||||
val theme = remember { mutableStateOf(null as String?) }
|
||||
val exportThemeLauncher = rememberFileChooserLauncher(false) { to: URI? ->
|
||||
val themeValue = theme.value
|
||||
if (themeValue != null && to != null) {
|
||||
copyBytesToFile(themeValue.byteInputStream(), to) {
|
||||
theme.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionItemView({
|
||||
val overrides = ThemeManager.currentThemeOverridesForExport(perChat, perUser)
|
||||
val lines = yaml.encodeToString<ThemeOverrides>(overrides).lines()
|
||||
// Removing theme id without using custom serializer or data class
|
||||
theme.value = lines.subList(1, lines.size).joinToString("\n")
|
||||
withLongRunningApi { exportThemeLauncher.launch("simplex.theme") }
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.export_theme), color = colors.primary)
|
||||
}
|
||||
val importThemeLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||
if (to != null) {
|
||||
val theme = getThemeFromUri(to)
|
||||
if (theme != null) {
|
||||
val res = ThemeModeOverride(mode = theme.base.mode, colors = theme.colors, wallpaper = theme.wallpaper?.importFromString()).removeSameColors(theme.base)
|
||||
save(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Can not limit to YAML mime type since it's unsupported by Android
|
||||
SectionItemView({ withLongRunningApi { importThemeLauncher.launch("*/*") } }) {
|
||||
Text(generalGetString(MR.strings.import_theme), color = colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResetToGlobalThemeButton(app: Boolean, onClick: () -> Unit) {
|
||||
SectionItemView(onClick) {
|
||||
Text(stringResource(MR.strings.chat_theme_reset_to_global_theme), color = MaterialTheme.colors.primary)
|
||||
Text(stringResource(if (app) MR.strings.chat_theme_reset_to_app_theme else MR.strings.chat_theme_reset_to_user_theme), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -146,6 +146,7 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri
|
||||
runCatching {
|
||||
return yaml.decodeFromStream<ThemeOverrides>(it!!)
|
||||
}.onFailure {
|
||||
Log.e(TAG, "Error while decoding theme: ${it.stackTraceToString()}")
|
||||
if (withAlertOnException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.import_theme_error),
|
||||
|
||||
+51
-24
@@ -40,6 +40,7 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.encodeToString
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
@@ -277,7 +278,7 @@ object AppearanceScope {
|
||||
if (themeUserDestination.value == null) {
|
||||
ThemeManager.saveAndApplyWallpaper(baseTheme, type)
|
||||
} else {
|
||||
val wallpaperFiles = listOf(perUserTheme.value.wallpaper?.imageFile)
|
||||
val wallpaperFiles = setOf(perUserTheme.value.wallpaper?.imageFile)
|
||||
ThemeManager.copyFromSameThemeOverrides(type, null, perUserTheme)
|
||||
val wallpaperFilesToDelete = wallpaperFiles - perUserTheme.value.wallpaper?.imageFile
|
||||
wallpaperFilesToDelete.forEach(::removeWallpaperFile)
|
||||
@@ -297,17 +298,15 @@ object AppearanceScope {
|
||||
saveThemeToDatabase(themeUserDestination.value)
|
||||
}
|
||||
|
||||
val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||
if (to != null) {
|
||||
val filename = saveWallpaperFile(to)
|
||||
if (filename != null) {
|
||||
if (themeUserDestination.value == null) {
|
||||
removeWallpaperFile((currentTheme.wallpaper.type as? WallpaperType.Image)?.filename)
|
||||
} else {
|
||||
removeWallpaperFile((perUserTheme.value.type as? WallpaperType.Image)?.filename)
|
||||
}
|
||||
onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL))
|
||||
val onImport = { to: URI ->
|
||||
val filename = saveWallpaperFile(to)
|
||||
if (filename != null) {
|
||||
if (themeUserDestination.value == null) {
|
||||
removeWallpaperFile((currentTheme.wallpaper.type as? WallpaperType.Image)?.filename)
|
||||
} else {
|
||||
removeWallpaperFile((perUserTheme.value.type as? WallpaperType.Image)?.filename)
|
||||
}
|
||||
onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,18 +318,18 @@ object AppearanceScope {
|
||||
ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get())
|
||||
}
|
||||
|
||||
val onChooseType: (WallpaperType?) -> Unit = { type: WallpaperType? ->
|
||||
val onChooseType: (WallpaperType?, FileChooserLauncher) -> Unit = { type: WallpaperType?, importWallpaperLauncher: FileChooserLauncher ->
|
||||
when {
|
||||
// don't have image in parent or already selected wallpaper with custom image
|
||||
type is WallpaperType.Image &&
|
||||
((wallpaperType is WallpaperType.Image && themeUserDestination.value?.second != null && chatModel.remoteHostId() == null) ||
|
||||
currentColors(type).wallpaper.type.image == null ||
|
||||
(currentColors(type).wallpaper.type.image != null && wallpaperType is WallpaperType.Image && themeUserDestination.value == null)) ->
|
||||
(currentColors(type).wallpaper.type.image != null && CurrentColors.value.wallpaper.type is WallpaperType.Image && themeUserDestination.value == null)) ->
|
||||
withLongRunningApi { importWallpaperLauncher.launch("image/*") }
|
||||
type is WallpaperType.Image && themeUserDestination.value == null -> onTypeChange(currentColors(type).wallpaper.type)
|
||||
type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing when remote host connected */ }
|
||||
type is WallpaperType.Image -> onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)
|
||||
(themeUserDestination.value != null && themeUserDestination.value?.second?.preferredMode(!CurrentColors.value.colors.isLight)?.type != type) || currentTheme.wallpaper.type != type -> onTypeCopyFromSameTheme(type)
|
||||
(themeUserDestination.value != null && themeUserDestination.value?.second?.preferredMode(!CurrentColors.value.colors.isLight)?.type != type) || CurrentColors.value.wallpaper.type != type -> onTypeCopyFromSameTheme(type)
|
||||
else -> onTypeChange(type)
|
||||
}
|
||||
}
|
||||
@@ -340,13 +339,17 @@ object AppearanceScope {
|
||||
ThemeDestinationPicker(themeUserDestination)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
|
||||
val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||
if (to != null) onImport(to)
|
||||
}
|
||||
|
||||
WallpaperPresetSelector(
|
||||
selectedWallpaper = wallpaperType,
|
||||
baseTheme = currentTheme.base,
|
||||
currentColors = { type ->
|
||||
currentColors(type)
|
||||
},
|
||||
onChooseType = onChooseType,
|
||||
onChooseType = { onChooseType(it, importWallpaperLauncher) },
|
||||
)
|
||||
val type = MaterialTheme.wallpaper.type
|
||||
if (type is WallpaperType.Image && (themeUserDestination.value == null || perUserTheme.value.wallpaper?.imageFile != null)) {
|
||||
@@ -400,7 +403,10 @@ object AppearanceScope {
|
||||
val user = themeUserDestination.value
|
||||
if (user == null) {
|
||||
ModalManager.start.showModal {
|
||||
CustomizeThemeView(onChooseType)
|
||||
val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||
if (to != null) onImport(to)
|
||||
}
|
||||
CustomizeThemeView { onChooseType(it, importWallpaperLauncher) }
|
||||
}
|
||||
} else {
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
@@ -586,19 +592,19 @@ object AppearanceScope {
|
||||
|
||||
@Composable
|
||||
fun ModalData.UserWallpaperEditorModal(remoteHostId: Long?, userId: Long, close: () -> Unit) {
|
||||
val themes = remember(chatModel.currentUser.value) { chatModel.currentUser.value?.uiThemes ?: ThemeModeOverrides() }
|
||||
val themes = remember(chatModel.currentUser.value) { mutableStateOf(chatModel.currentUser.value?.uiThemes ?: ThemeModeOverrides()) }
|
||||
val globalThemeUsed = remember { stateGetOrPut("globalThemeUsed") { false } }
|
||||
val initialTheme = remember(CurrentColors.collectAsState().value.base) {
|
||||
val preferred = themes.preferredMode(!CurrentColors.value.colors.isLight)
|
||||
val preferred = themes.value.preferredMode(!CurrentColors.value.colors.isLight)
|
||||
globalThemeUsed.value = preferred == null
|
||||
preferred ?: ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||
}
|
||||
UserWallpaperEditor(
|
||||
initialTheme,
|
||||
applyToMode = if (themes.light == themes.dark) null else initialTheme.mode,
|
||||
applyToMode = if (themes.value.light == themes.value.dark) null else initialTheme.mode,
|
||||
globalThemeUsed = globalThemeUsed,
|
||||
save = { applyToMode, newTheme ->
|
||||
save(applyToMode, newTheme, themes, userId, remoteHostId)
|
||||
save(applyToMode, newTheme, themes.value, userId, remoteHostId)
|
||||
})
|
||||
KeyChangeEffect(chatModel.currentUser.value?.userId, chatModel.remoteHostId) {
|
||||
close()
|
||||
@@ -613,7 +619,7 @@ object AppearanceScope {
|
||||
remoteHostId: Long?
|
||||
) {
|
||||
val unchangedThemes: ThemeModeOverrides = themes ?: ThemeModeOverrides()
|
||||
val wallpaperFiles = listOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile)
|
||||
val wallpaperFiles = setOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile)
|
||||
var changedThemes: ThemeModeOverrides? = unchangedThemes
|
||||
val changed = newTheme?.copy(wallpaper = newTheme.wallpaper?.withFilledWallpaperPath())
|
||||
changedThemes = when (applyToMode) {
|
||||
@@ -621,7 +627,28 @@ object AppearanceScope {
|
||||
DefaultThemeMode.LIGHT -> changedThemes?.copy(light = changed?.copy(mode = applyToMode))
|
||||
DefaultThemeMode.DARK -> changedThemes?.copy(dark = changed?.copy(mode = applyToMode))
|
||||
}
|
||||
changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) changedThemes else null
|
||||
changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) {
|
||||
val light = changedThemes.light
|
||||
val dark = changedThemes.dark
|
||||
val currentMode = CurrentColors.value.base.mode
|
||||
// same image file for both modes, copy image to make them as different files
|
||||
if (light?.wallpaper?.imageFile != null && dark?.wallpaper?.imageFile != null && light.wallpaper.imageFile == dark.wallpaper.imageFile) {
|
||||
val imageFile = if (currentMode == DefaultThemeMode.LIGHT) {
|
||||
dark.wallpaper.imageFile
|
||||
} else {
|
||||
light.wallpaper.imageFile
|
||||
}
|
||||
val filePath = saveWallpaperFile(File(getWallpaperFilePath(imageFile)).toURI())
|
||||
changedThemes = if (currentMode == DefaultThemeMode.LIGHT) {
|
||||
changedThemes.copy(dark = dark.copy(wallpaper = dark.wallpaper.copy(imageFile = filePath)))
|
||||
} else {
|
||||
changedThemes.copy(light = light.copy(wallpaper = light.wallpaper.copy(imageFile = filePath)))
|
||||
}
|
||||
}
|
||||
changedThemes
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val wallpaperFilesToDelete = wallpaperFiles - changedThemes?.light?.wallpaper?.imageFile - changedThemes?.dark?.wallpaper?.imageFile
|
||||
wallpaperFilesToDelete.forEach(::removeWallpaperFile)
|
||||
@@ -662,9 +689,9 @@ object AppearanceScope {
|
||||
}
|
||||
|
||||
val values by remember(chatModel.users.toList()) { mutableStateOf(
|
||||
listOf(null as Long? to generalGetString(MR.strings.theme_destination_all_profiles))
|
||||
listOf(null as Long? to generalGetString(MR.strings.theme_destination_app_theme))
|
||||
+
|
||||
chatModel.users.filter { it.user.activeUser || it.user.viewPwdHash == null }.map {
|
||||
chatModel.users.filter { it.user.activeUser }.map {
|
||||
it.user.userId to it.user.chatViewName
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1570,7 +1570,7 @@
|
||||
<string name="export_theme">Export theme</string>
|
||||
<string name="reset_color">Reset colors</string>
|
||||
<string name="reset_single_color">Reset color</string>
|
||||
<string name="theme_destination_all_profiles">All chat profiles</string>
|
||||
<string name="theme_destination_app_theme">App theme</string>
|
||||
<string name="color_primary">Accent</string>
|
||||
<string name="color_primary_variant">Additional accent</string>
|
||||
<string name="color_secondary">Secondary</string>
|
||||
@@ -1601,7 +1601,8 @@
|
||||
<string name="wallpaper_scale_fill">Fill</string>
|
||||
<string name="wallpaper_scale_fit">Fit</string>
|
||||
<string name="wallpaper_advanced_settings">Advanced settings</string>
|
||||
<string name="chat_theme_reset_to_global_theme">Reset to global theme</string>
|
||||
<string name="chat_theme_reset_to_app_theme">Reset to app theme</string>
|
||||
<string name="chat_theme_reset_to_user_theme">Reset to user theme</string>
|
||||
<string name="chat_theme_set_default_theme">Set default theme</string>
|
||||
<string name="chat_theme_apply_to_mode">Apply to</string>
|
||||
<string name="chat_theme_apply_to_all_modes">All color modes</string>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: bd67844169d2206d8543c01e6ed966315115b0e3
|
||||
tag: e2f4ffc9db8e3812cfaf12ded9904061fb330fd6
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."bd67844169d2206d8543c01e6ed966315115b0e3" = "1g218q15hrg21h8gyidavfys5zx8dzmxq7iwfm5bfaw71grpd7pn";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."e2f4ffc9db8e3812cfaf12ded9904061fb330fd6" = "0mvg8wn8brcw71fh0lsf39lypha2kfil443bq7gz6zhwl87vaz28";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
||||
+30
-29
@@ -1550,8 +1550,8 @@ processChatCommand' vr = \case
|
||||
let chatV = agentToChatVersion agentV
|
||||
dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend
|
||||
connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup'
|
||||
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode chatV pqSup'
|
||||
void . withAgent $ \a -> joinConnection a (aUserId user) (Just connId) True cReq dm pqSup' subMode
|
||||
conn@PendingContactConnection {pccConnId} <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode chatV pqSup'
|
||||
joinPreparedAgentConnection user pccConnId connId cReq dm pqSup' subMode
|
||||
pure $ CRSentConfirmation user conn
|
||||
APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq
|
||||
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
|
||||
@@ -1806,12 +1806,20 @@ processChatCommand' vr = \case
|
||||
dm <- encodeConnInfo $ XGrpAcpt membershipMemId
|
||||
agentConnId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff
|
||||
let chatV = vr `peerConnChatVersion` peerChatVRange
|
||||
withStore' $ \db -> do
|
||||
createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode
|
||||
cId <- withStore' $ \db -> do
|
||||
Connection {connId = cId} <- createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode
|
||||
updateGroupMemberStatus db userId fromMember GSMemAccepted
|
||||
updateGroupMemberStatus db userId membership GSMemAccepted
|
||||
void . withAgent $ \a -> joinConnection a (aUserId user) (Just agentConnId) True connRequest dm PQSupportOff subMode
|
||||
updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` \_ -> pure ()
|
||||
pure cId
|
||||
void (withAgent $ \a -> joinConnection a (aUserId user) (Just agentConnId) True connRequest dm PQSupportOff subMode)
|
||||
`catchChatError` \e -> do
|
||||
withStore' $ \db -> do
|
||||
deleteConnectionRecord db user cId
|
||||
updateGroupMemberStatus db userId fromMember GSMemInvited
|
||||
updateGroupMemberStatus db userId membership GSMemInvited
|
||||
withAgent $ \a -> deleteConnectionAsync a False agentConnId
|
||||
throwError e
|
||||
updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` (toView . CRChatError (Just user))
|
||||
pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing
|
||||
Nothing -> throwChatError $ CEContactNotActive ct
|
||||
APIMemberRole groupId memberId memRole -> withUser $ \user -> do
|
||||
@@ -2225,20 +2233,7 @@ processChatCommand' vr = \case
|
||||
counts <- map (first decodeLatin1) <$> withAgent' getMsgCounts
|
||||
let allMsgs = foldl' (\(ts, ds) (_, (t, d)) -> (ts + t, ds + d)) (0, 0) counts
|
||||
pure CRAgentMsgCounts {msgCounts = ("all", allMsgs) : sortOn (Down . snd) (filter (\(_, (_, d)) -> d /= 0) counts)}
|
||||
GetAgentSubs -> lift $ summary <$> withAgent' getAgentSubscriptions
|
||||
where
|
||||
summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} =
|
||||
CRAgentSubs
|
||||
{ activeSubs = foldl' countSubs M.empty activeSubscriptions,
|
||||
pendingSubs = foldl' countSubs M.empty pendingSubscriptions,
|
||||
removedSubs = foldl' accSubErrors M.empty removedSubscriptions
|
||||
}
|
||||
where
|
||||
countSubs m SubInfo {server} = M.alter (Just . maybe 1 (+ 1)) server m
|
||||
accSubErrors m = \case
|
||||
SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m
|
||||
_ -> m
|
||||
GetAgentSubsDetails -> lift $ CRAgentSubsDetails <$> withAgent' getAgentSubscriptions
|
||||
GetAgentSubs diff -> withUser $ \User {userId} -> lift $ CRAgentSubs <$> withAgent' (\a -> getAgentSubscriptions a diff (Just userId))
|
||||
-- CustomChatCommand is unsupported, it can be processed in preCmdHook
|
||||
-- in a modified CLI app or core - the hook should return Either ChatResponse ChatCommand
|
||||
CustomChatCommand _cmd -> withUser $ \user -> pure $ chatCmdError (Just user) "not supported"
|
||||
@@ -2338,8 +2333,8 @@ processChatCommand' vr = \case
|
||||
-- [incognito] generate profile to send
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup
|
||||
joinContact user connId cReq incognitoProfile xContactId inGroup pqSup chatV
|
||||
conn@PendingContactConnection {pccConnId} <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup
|
||||
joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV
|
||||
pure $ CRSentInvitation user conn incognitoProfile
|
||||
connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> ConnectionRequestUri 'CMContact -> CM ChatResponse
|
||||
connectContactViaAddress user incognito ct cReq =
|
||||
@@ -2351,8 +2346,8 @@ processChatCommand' vr = \case
|
||||
-- [incognito] generate profile to send
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
ct' <- withStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash newXContactId incognitoProfile subMode chatV pqSup
|
||||
joinContact user connId cReq incognitoProfile newXContactId False pqSup chatV
|
||||
(pccConnId, ct') <- withStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash newXContactId incognitoProfile subMode chatV pqSup
|
||||
joinContact user pccConnId connId cReq incognitoProfile newXContactId False pqSup chatV
|
||||
pure $ CRSentInvitationToContact user ct' incognitoProfile
|
||||
prepareContact :: User -> ConnectionRequestUri 'CMContact -> PQSupport -> CM (ConnId, VersionChat)
|
||||
prepareContact user cReq pqSup = do
|
||||
@@ -2365,12 +2360,19 @@ processChatCommand' vr = \case
|
||||
let chatV = agentToChatVersion agentV
|
||||
connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup
|
||||
pure (connId, chatV)
|
||||
joinContact :: User -> ConnId -> ConnectionRequestUri 'CMContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM ()
|
||||
joinContact user connId cReq incognitoProfile xContactId inGroup pqSup chatV = do
|
||||
joinContact :: User -> Int64 -> ConnId -> ConnectionRequestUri 'CMContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM ()
|
||||
joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV = do
|
||||
let profileToSend = userProfileToSend user incognitoProfile Nothing inGroup
|
||||
dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend $ Just xContactId)
|
||||
subMode <- chatReadVar subscriptionMode
|
||||
void . withAgent $ \a -> joinConnection a (aUserId user) (Just connId) True cReq dm pqSup subMode
|
||||
joinPreparedAgentConnection user pccConnId connId cReq dm pqSup subMode
|
||||
joinPreparedAgentConnection :: User -> Int64 -> ConnId -> ConnectionRequestUri m -> ByteString -> PQSupport -> SubscriptionMode -> CM ()
|
||||
joinPreparedAgentConnection user pccConnId connId cReq connInfo pqSup subMode = do
|
||||
void (withAgent $ \a -> joinConnection a (aUserId user) (Just connId) True cReq connInfo pqSup subMode)
|
||||
`catchChatError` \e -> do
|
||||
withStore' $ \db -> deleteConnectionRecord db user pccConnId
|
||||
withAgent $ \a -> deleteConnectionAsync a False connId
|
||||
throwError e
|
||||
contactMember :: Contact -> [GroupMember] -> Maybe GroupMember
|
||||
contactMember Contact {contactId} =
|
||||
find $ \GroupMember {memberContactId = cId, memberStatus = s} ->
|
||||
@@ -7474,8 +7476,7 @@ chatCommandP =
|
||||
"/debug event " *> (DebugEvent <$> jsonP),
|
||||
"/get stats" $> GetAgentStats,
|
||||
"/reset stats" $> ResetAgentStats,
|
||||
"/get subs" $> GetAgentSubs,
|
||||
"/get subs details" $> GetAgentSubsDetails,
|
||||
"/get subs" *> (GetAgentSubs <$> (" diff" $> True <|> pure False)),
|
||||
"/get workers" $> GetAgentWorkers,
|
||||
"/get workers details" $> GetAgentWorkersDetails,
|
||||
"/get msgs" $> GetAgentMsgCounts,
|
||||
|
||||
@@ -499,8 +499,7 @@ data ChatCommand
|
||||
| DebugEvent ChatResponse
|
||||
| GetAgentStats
|
||||
| ResetAgentStats
|
||||
| GetAgentSubs
|
||||
| GetAgentSubsDetails
|
||||
| GetAgentSubs {diffOnly :: Bool}
|
||||
| GetAgentWorkers
|
||||
| GetAgentWorkersDetails
|
||||
| GetAgentMsgCounts
|
||||
@@ -747,8 +746,7 @@ data ChatResponse
|
||||
| CRAgentStats {agentStats :: [[String]]}
|
||||
| CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails}
|
||||
| CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary}
|
||||
| CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]}
|
||||
| CRAgentSubsDetails {agentSubs :: SubscriptionsInfo}
|
||||
| CRAgentSubs {agentSubs :: SubscriptionsInfo}
|
||||
| CRAgentMsgCounts {msgCounts :: [(Text, (Int, Int))]}
|
||||
| CRContactDisabled {user :: User, contact :: Contact}
|
||||
| CRConnectionDisabled {connectionEntity :: ConnectionEntity}
|
||||
|
||||
@@ -14,6 +14,7 @@ module Simplex.Chat.Store.Connections
|
||||
getContactConnEntityByConnReqHash,
|
||||
getConnectionsToSubscribe,
|
||||
unsetConnectionToSubscribe,
|
||||
deleteConnectionRecord,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -225,3 +226,7 @@ getConnectionsToSubscribe db vr = do
|
||||
|
||||
unsetConnectionToSubscribe :: DB.Connection -> IO ()
|
||||
unsetConnectionToSubscribe db = DB.execute_ db "UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1"
|
||||
|
||||
deleteConnectionRecord :: DB.Connection -> User -> Int64 -> IO ()
|
||||
deleteConnectionRecord db User {userId} cId = do
|
||||
DB.execute db "DELETE FROM connections WHERE user_id = ? AND connection_id = ?" (userId, cId)
|
||||
|
||||
@@ -130,11 +130,11 @@ deletePendingContactConnection db userId connId =
|
||||
|]
|
||||
(userId, connId, ConnContact)
|
||||
|
||||
createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO Contact
|
||||
createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO (Int64, Contact)
|
||||
createAddressContactConnection db vr user@User {userId} Contact {contactId} acId cReqHash xContactId incognitoProfile subMode chatV pqSup = do
|
||||
PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile Nothing subMode chatV pqSup
|
||||
liftIO $ DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, pccConnId)
|
||||
getContact db vr user contactId
|
||||
(pccConnId,) <$> getContact db vr user contactId
|
||||
|
||||
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection
|
||||
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup = do
|
||||
|
||||
@@ -944,10 +944,10 @@ getMemberInvitation db User {userId} groupMemberId =
|
||||
fmap join . maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId)
|
||||
|
||||
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO ()
|
||||
createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO Connection
|
||||
createMemberConnection db userId GroupMember {groupMemberId} agentConnId chatV peerChatVRange subMode = do
|
||||
currentTs <- getCurrentTime
|
||||
void $ createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode
|
||||
createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode
|
||||
|
||||
createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO ()
|
||||
createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) chatV peerChatVRange subMode = do
|
||||
@@ -1090,20 +1090,33 @@ createIntroductions db chatV members toMember = do
|
||||
then pure []
|
||||
else do
|
||||
currentTs <- getCurrentTime
|
||||
mapM (insertIntro_ currentTs) reMembers
|
||||
catMaybes <$> mapM (createIntro_ currentTs) reMembers
|
||||
where
|
||||
insertIntro_ :: UTCTime -> GroupMember -> IO GroupMemberIntro
|
||||
insertIntro_ ts reMember = do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_member_intros
|
||||
(re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
|]
|
||||
(groupMemberId' reMember, groupMemberId' toMember, GMIntroPending, chatV, ts, ts)
|
||||
introId <- insertedRowId db
|
||||
pure GroupMemberIntro {introId, reMember, toMember, introStatus = GMIntroPending, introInvitation = Nothing}
|
||||
createIntro_ :: UTCTime -> GroupMember -> IO (Maybe GroupMemberIntro)
|
||||
createIntro_ ts reMember =
|
||||
-- when members connect concurrently, host would try to create introductions between them in both directions;
|
||||
-- this check avoids creating second (redundant) introduction
|
||||
checkInverseIntro >>= \case
|
||||
Just _ -> pure Nothing
|
||||
Nothing -> do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO group_member_intros
|
||||
(re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
|]
|
||||
(groupMemberId' reMember, groupMemberId' toMember, GMIntroPending, chatV, ts, ts)
|
||||
introId <- insertedRowId db
|
||||
pure $ Just GroupMemberIntro {introId, reMember, toMember, introStatus = GMIntroPending, introInvitation = Nothing}
|
||||
where
|
||||
checkInverseIntro :: IO (Maybe Int64)
|
||||
checkInverseIntro =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
"SELECT 1 FROM group_member_intros WHERE re_group_member_id = ? AND to_group_member_id = ? LIMIT 1"
|
||||
(groupMemberId' toMember, groupMemberId' reMember)
|
||||
|
||||
updateIntroStatus :: DB.Connection -> Int64 -> GroupMemberIntroStatus -> IO ()
|
||||
updateIntroStatus db introId introStatus = do
|
||||
|
||||
+19
-24
@@ -22,7 +22,6 @@ import Data.Int (Int64)
|
||||
import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
|
||||
import Data.List.NonEmpty (NonEmpty (..))
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Map.Strict (Map)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
@@ -52,7 +51,7 @@ import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.Types.UITheme
|
||||
import qualified Simplex.FileTransfer.Transport as XFTP
|
||||
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..))
|
||||
import Simplex.Messaging.Agent.Client (ActivePendingSubs (..), ProtocolTestFailure (..), ProtocolTestStep (..), SubInfo (..), SubscriptionsInfo (..))
|
||||
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||
@@ -358,18 +357,14 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
||||
plain $ "agent locks: " <> LB.unpack (J.encode agentLocks)
|
||||
]
|
||||
CRAgentStats stats -> map (plain . intercalate ",") stats
|
||||
CRAgentSubs {activeSubs, pendingSubs, removedSubs} ->
|
||||
[plain $ "Subscriptions: active = " <> show (sum activeSubs) <> ", pending = " <> show (sum pendingSubs) <> ", removed = " <> show (sum $ M.map length removedSubs)]
|
||||
<> ("active subscriptions:" : listSubs activeSubs)
|
||||
<> ("pending subscriptions:" : listSubs pendingSubs)
|
||||
<> ("removed subscriptions:" : listSubs removedSubs)
|
||||
CRAgentSubs SubscriptionsInfo {summary, servers} -> "Subscriptions:" : activePending summary <> concatMap (uncurry byServer) (M.assocs servers)
|
||||
where
|
||||
listSubs :: Show a => Map Text a -> [StyledString]
|
||||
listSubs = map (\(srv, info) -> plain $ srv <> ": " <> tshow info) . M.assocs
|
||||
CRAgentSubsDetails SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} ->
|
||||
("active subscriptions:" : map sShow activeSubscriptions)
|
||||
<> ("pending subscriptions: " : map sShow pendingSubscriptions)
|
||||
<> ("removed subscriptions: " : map sShow removedSubscriptions)
|
||||
byServer srv aps = plain srv : activePending aps
|
||||
activePending ActivePendingSubs {active_, pending_} =
|
||||
[ " active: " <> subInfo active_,
|
||||
" pending: " <> subInfo pending_
|
||||
]
|
||||
subInfo SubInfo {count, clientsMissing, clientsExtra} = sShow count <> " (clients: -" <> sShow clientsMissing <> " +" <> sShow clientsExtra <> ")"
|
||||
CRAgentWorkersSummary {agentWorkersSummary} -> ["agent workers summary: " <> plain (LB.unpack $ J.encode agentWorkersSummary)]
|
||||
CRAgentWorkersDetails {agentWorkersDetails} ->
|
||||
[ "agent workers details:",
|
||||
@@ -388,9 +383,9 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
||||
CRAgentConnDeleted acId -> ["completed deleting connection, agent connection id: " <> sShow acId | logLevel <= CLLInfo]
|
||||
CRAgentUserDeleted auId -> ["completed deleting user" <> if logLevel <= CLLInfo then ", agent user id: " <> sShow auId else ""]
|
||||
CRMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning]
|
||||
CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError logLevel testView e
|
||||
CRChatError u e -> ttyUser' u $ viewChatError logLevel testView e
|
||||
CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError logLevel testView) errs
|
||||
CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError True logLevel testView e
|
||||
CRChatError u e -> ttyUser' u $ viewChatError False logLevel testView e
|
||||
CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError False logLevel testView) errs
|
||||
CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)]
|
||||
CRAppSettings as -> ["app settings: " <> plain (LB.unpack $ J.encode as)]
|
||||
CRTimedAction _ _ -> []
|
||||
@@ -1894,8 +1889,8 @@ viewRemoteCtrl CtrlAppInfo {deviceName, appVersionRange = AppVersionRange _ (App
|
||||
| otherwise = ""
|
||||
showCompatible = if compatible then "" else ", " <> bold' "not compatible"
|
||||
|
||||
viewChatError :: ChatLogLevel -> Bool -> ChatError -> [StyledString]
|
||||
viewChatError logLevel testView = \case
|
||||
viewChatError :: Bool -> ChatLogLevel -> Bool -> ChatError -> [StyledString]
|
||||
viewChatError isCmd logLevel testView = \case
|
||||
ChatError err -> case err of
|
||||
CENoActiveUser -> ["error: active user is required"]
|
||||
CENoConnectionUser agentConnId -> ["error: message user not found, conn id: " <> sShow agentConnId | logLevel <= CLLError]
|
||||
@@ -2034,14 +2029,14 @@ viewChatError logLevel testView = \case
|
||||
<> "error: connection authorization failed - this could happen if connection was deleted,\
|
||||
\ secured with different credentials, or due to a bug - please re-create the connection"
|
||||
]
|
||||
BROKER _ NETWORK -> []
|
||||
BROKER _ TIMEOUT -> []
|
||||
AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug]
|
||||
AGENT (A_PROHIBITED e) -> [withConnEntity <> "error: AGENT A_PROHIBITED, " <> plain e | logLevel <= CLLWarning]
|
||||
CONN NOT_FOUND -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning]
|
||||
BROKER _ NETWORK | not isCmd -> []
|
||||
BROKER _ TIMEOUT | not isCmd -> []
|
||||
AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd]
|
||||
AGENT (A_PROHIBITED e) -> [withConnEntity <> "error: AGENT A_PROHIBITED, " <> plain e | logLevel <= CLLWarning || isCmd]
|
||||
CONN NOT_FOUND -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning || isCmd]
|
||||
CRITICAL restart e -> [plain $ "critical error: " <> e] <> ["please restart the app" | restart]
|
||||
INTERNAL e -> [plain $ "internal error: " <> e]
|
||||
e -> [withConnEntity <> "smp agent error: " <> sShow e | logLevel <= CLLWarning]
|
||||
e -> [withConnEntity <> "smp agent error: " <> sShow e | logLevel <= CLLWarning || isCmd]
|
||||
where
|
||||
withConnEntity = case entity_ of
|
||||
Just entity@(RcvDirectMsgConnection conn contact_) -> case contact_ of
|
||||
|
||||
Reference in New Issue
Block a user