Compare commits
12 Commits
ep/survey-
...
av/prepare
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e83454a558 | ||
|
|
ea1aaf6a0a | ||
|
|
976fc68cc3 | ||
|
|
7c7e931aa9 | ||
|
|
e8e619effa | ||
|
|
bd0139eaab | ||
|
|
e9f77e1064 | ||
|
|
677b75f368 | ||
|
|
92d13591f3 | ||
|
|
6be8476f90 | ||
|
|
ae9b83515c | ||
|
|
26a233ab1a |
@@ -127,7 +127,7 @@ public enum ChatCommand {
|
||||
case let .setAllContactReceipts(enable): return "/set receipts all \(onOff(enable))"
|
||||
case let .apiSetUserContactReceipts(userId, userMsgReceiptSettings):
|
||||
let umrs = userMsgReceiptSettings
|
||||
return "/_set receipts \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))"
|
||||
return "/_set receipts contacts \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))"
|
||||
case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))"
|
||||
case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))"
|
||||
case let .apiMuteUser(userId): return "/_mute user \(userId)"
|
||||
|
||||
@@ -55,6 +55,7 @@ allprojects {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||
maven("https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ kotlin {
|
||||
val desktopMain by getting {
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.1")
|
||||
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.6")
|
||||
implementation("org.slf4j:slf4j-simple:2.0.7")
|
||||
}
|
||||
}
|
||||
val desktopTest by getting
|
||||
|
||||
@@ -25,7 +25,7 @@ val defaultLocale: Locale = Locale.getDefault()
|
||||
lateinit var androidAppContext: Context
|
||||
lateinit var mainActivity: WeakReference<FragmentActivity>
|
||||
|
||||
actual fun initHaskell() {
|
||||
fun initHaskell() {
|
||||
val socketName = "chat.simplex.app.local.socket.address.listen.native.cmd2" + Random.nextLong(100000)
|
||||
val s = Semaphore(0)
|
||||
thread(name="stdout/stderr pipe") {
|
||||
|
||||
@@ -48,4 +48,6 @@ actual fun screenOrientation(): ScreenOrientation = when (mainActivity.get()?.re
|
||||
@Composable
|
||||
actual fun screenWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
|
||||
|
||||
actual fun desktopExpandWindowToWidth(width: Dp) {}
|
||||
|
||||
actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package chat.simplex.common.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun isSystemInDarkTheme(): Boolean = androidx.compose.foundation.isSystemInDarkTheme()
|
||||
@@ -35,7 +35,7 @@ actual fun SimpleAndAnimatedImageView(
|
||||
ImageView(imagePainter) {
|
||||
hideKeyboard(view)
|
||||
if (getLoadedFilePath(file) != null) {
|
||||
ModalManager.shared.showCustomModal(animated = false) { close ->
|
||||
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
|
||||
ImageFullScreenView(imageProvider, close)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
|
||||
actual fun Modifier.onRightClick(action: () -> Unit): Modifier = this
|
||||
|
||||
actual interface DefaultExposedDropdownMenuBoxScope {
|
||||
@Composable
|
||||
actual fun DefaultExposedDropdownMenu(
|
||||
|
||||
@@ -69,7 +69,7 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod
|
||||
changeIcon = ::setAppIcon,
|
||||
showSettingsModal = showSettingsModal,
|
||||
editColor = { name, initialColor ->
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
ColorEditor(name, initialColor, close)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,6 +19,8 @@ project("app")
|
||||
if(UNIX AND NOT APPLE)
|
||||
set(OS_LIB_PATH "linux")
|
||||
set(OS_LIB_EXT "so")
|
||||
# Makes ld search libs in the same dir as libapp-lib, not in system dirs
|
||||
set(CMAKE_BUILD_RPATH "$ORIGIN")
|
||||
elseif(WIN32)
|
||||
set(OS_LIB_PATH "windows")
|
||||
set(OS_LIB_EXT "dll")
|
||||
@@ -37,9 +39,6 @@ else()
|
||||
set(OS_LIB_ARCH "${CMAKE_SYSTEM_PROCESSOR}")
|
||||
endif()
|
||||
|
||||
# Makes ld search libs in the same dir as libapp-lib, not in system dirs
|
||||
#set(CMAKE_BUILD_RPATH "$ORIGIN")
|
||||
|
||||
# Creates and names a library, sets it as either STATIC
|
||||
# or SHARED, and provides the relative paths to its source code.
|
||||
# You can define multiple libraries, and CMake builds them for you.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <jni.h>
|
||||
#include <string.h>
|
||||
|
||||
// from the RTS
|
||||
void hs_init(int * argc, char **argv[]);
|
||||
@@ -30,13 +31,35 @@ extern char *chat_parse_markdown(const char *str);
|
||||
extern char *chat_parse_server(const char *str);
|
||||
extern char *chat_password_hash(const char *pwd, const char *salt);
|
||||
|
||||
|
||||
// As a reference: https://stackoverflow.com/a/60002045
|
||||
jstring correct_string_utf8(JNIEnv *env, char *string) {
|
||||
jobject bb = (*env)->NewDirectByteBuffer(env, (void *)string, strlen(string));
|
||||
jclass cls_charset = (*env)->FindClass(env, "java/nio/charset/Charset");
|
||||
jmethodID mid_charset_forName = (*env)->GetStaticMethodID(env, cls_charset, "forName", "(Ljava/lang/String;)Ljava/nio/charset/Charset;");
|
||||
jobject charset = (*env)->CallStaticObjectMethod(env, cls_charset, mid_charset_forName, (*env)->NewStringUTF(env, "UTF-8"));
|
||||
|
||||
jmethodID mid_decode = (*env)->GetMethodID(env, cls_charset, "decode", "(Ljava/nio/ByteBuffer;)Ljava/nio/CharBuffer;");
|
||||
jobject cb = (*env)->CallObjectMethod(env, charset, mid_decode, bb);
|
||||
|
||||
jclass cls_char_buffer = (*env)->FindClass(env, "java/nio/CharBuffer");
|
||||
jmethodID mid_to_string = (*env)->GetMethodID(env, cls_char_buffer, "toString", "()Ljava/lang/String;");
|
||||
jstring res = (*env)->CallObjectMethod(env, cb, mid_to_string);
|
||||
|
||||
(*env)->DeleteLocalRef(env, bb);
|
||||
(*env)->DeleteLocalRef(env, charset);
|
||||
(*env)->DeleteLocalRef(env, cb);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) {
|
||||
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
|
||||
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
|
||||
const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE);
|
||||
jlong _ctrl = (jlong) 0;
|
||||
jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl));
|
||||
jstring res = correct_string_utf8(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl));
|
||||
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
|
||||
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
|
||||
(*env)->ReleaseStringUTFChars(env, dbKey, _confirm);
|
||||
@@ -56,25 +79,25 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, jclass cla
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, jclass clazz, jlong controller, jstring msg) {
|
||||
const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
|
||||
jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg));
|
||||
jstring res = correct_string_utf8(env, chat_send_cmd((void*)controller, _msg));
|
||||
(*env)->ReleaseStringUTFChars(env, msg, _msg);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatRecvMsg(JNIEnv *env, jclass clazz, jlong controller) {
|
||||
return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller));
|
||||
return correct_string_utf8(env, chat_recv_msg((void*)controller));
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatRecvMsgWait(JNIEnv *env, jclass clazz, jlong controller, jint wait) {
|
||||
return (*env)->NewStringUTF(env, chat_recv_msg_wait((void*)controller, wait));
|
||||
return correct_string_utf8(env, chat_recv_msg_wait((void*)controller, wait));
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatParseMarkdown(JNIEnv *env, jclass clazz, jstring str) {
|
||||
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
|
||||
jstring res = (*env)->NewStringUTF(env, chat_parse_markdown(_str));
|
||||
jstring res = correct_string_utf8(env, chat_parse_markdown(_str));
|
||||
(*env)->ReleaseStringUTFChars(env, str, _str);
|
||||
return res;
|
||||
}
|
||||
@@ -82,7 +105,7 @@ Java_chat_simplex_common_platform_CoreKt_chatParseMarkdown(JNIEnv *env, jclass c
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, jclass clazz, jstring str) {
|
||||
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
|
||||
jstring res = (*env)->NewStringUTF(env, chat_parse_server(_str));
|
||||
jstring res = correct_string_utf8(env, chat_parse_server(_str));
|
||||
(*env)->ReleaseStringUTFChars(env, str, _str);
|
||||
return res;
|
||||
}
|
||||
@@ -91,7 +114,7 @@ JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, jclass clazz, jstring pwd, jstring salt) {
|
||||
const char *_pwd = (*env)->GetStringUTFChars(env, pwd, JNI_FALSE);
|
||||
const char *_salt = (*env)->GetStringUTFChars(env, salt, JNI_FALSE);
|
||||
jstring res = (*env)->NewStringUTF(env, chat_password_hash(_pwd, _salt));
|
||||
jstring res = correct_string_utf8(env, chat_password_hash(_pwd, _salt));
|
||||
(*env)->ReleaseStringUTFChars(env, pwd, _pwd);
|
||||
(*env)->ReleaseStringUTFChars(env, salt, _salt);
|
||||
return res;
|
||||
|
||||
@@ -1,35 +1,46 @@
|
||||
package chat.simplex.common
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.SimpleButton
|
||||
import chat.simplex.common.views.SplashView
|
||||
import chat.simplex.common.views.call.ActiveCallView
|
||||
import chat.simplex.common.views.call.IncomingCallAlertView
|
||||
import chat.simplex.common.views.chat.ChatView
|
||||
import chat.simplex.common.views.chatlist.ChatListView
|
||||
import chat.simplex.common.views.chatlist.ShareListView
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.database.DatabaseErrorView
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.localauth.VerticalDivider
|
||||
import chat.simplex.common.views.onboarding.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
data class SettingsViewState(
|
||||
val userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||
val scaffoldState: ScaffoldState,
|
||||
val switchingUsers: MutableState<Boolean>
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun AppScreen() {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
@@ -66,7 +77,7 @@ fun MainScreen() {
|
||||
}
|
||||
LaunchedEffect(chatModel.clearOverlays.value) {
|
||||
if (chatModel.clearOverlays.value) {
|
||||
ModalManager.shared.closeModals()
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
chatModel.clearOverlays.value = false
|
||||
}
|
||||
}
|
||||
@@ -105,61 +116,30 @@ fun MainScreen() {
|
||||
onboarding == OnboardingStage.OnboardingComplete && userCreated -> {
|
||||
Box {
|
||||
showAdvertiseLAAlert = true
|
||||
BoxWithConstraints {
|
||||
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
|
||||
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
|
||||
Box(
|
||||
Modifier
|
||||
.graphicsLayer {
|
||||
translationX = -offset.value.dp.toPx()
|
||||
}
|
||||
) {
|
||||
if (chatModel.setDeliveryReceipts.value) {
|
||||
SetDeliveryReceiptsView(chatModel)
|
||||
} else {
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
if (chatModel.sharedContent.value == null)
|
||||
ChatListView(chatModel, AppLock::setPerformLA, stopped)
|
||||
else
|
||||
ShareListView(chatModel, stopped)
|
||||
}
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
val onComposed: () -> Unit = {
|
||||
scope.launch {
|
||||
offset.animateTo(
|
||||
if (chatModel.chatId.value == null) 0f else maxWidth.value,
|
||||
chatListAnimationSpec()
|
||||
)
|
||||
if (offset.value == 0f) {
|
||||
currentChatId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
if (it != null) currentChatId = it
|
||||
else onComposed()
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@{
|
||||
currentChatId?.let {
|
||||
ChatView(it, chatModel, onComposed)
|
||||
}
|
||||
}
|
||||
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val switchingUsers = rememberSaveable { mutableStateOf(false) }
|
||||
val settingsState = remember { SettingsViewState(userPickerState, scaffoldState, switchingUsers) }
|
||||
if (appPlatform.isAndroid) {
|
||||
AndroidScreen(settingsState)
|
||||
} else {
|
||||
DesktopScreen(settingsState)
|
||||
}
|
||||
}
|
||||
}
|
||||
onboarding == OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true)
|
||||
onboarding == OnboardingStage.Step1_SimpleXInfo -> {
|
||||
SimpleXInfo(chatModel, onboarding = true)
|
||||
if (appPlatform.isDesktop) {
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
}
|
||||
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {}
|
||||
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel)
|
||||
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
|
||||
}
|
||||
ModalManager.shared.showInView()
|
||||
if (appPlatform.isAndroid) {
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
|
||||
val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } }
|
||||
if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) {
|
||||
@@ -178,7 +158,7 @@ fun MainScreen() {
|
||||
} else if (chatModel.showCallView.value) {
|
||||
ActiveCallView()
|
||||
}
|
||||
ModalManager.shared.showPasscodeInView()
|
||||
ModalManager.fullscreen.showPasscodeInView()
|
||||
val invitation = chatModel.activeCallInvitation.value
|
||||
if (invitation != null) IncomingCallAlertView(invitation, chatModel)
|
||||
AlertManager.shared.showInView()
|
||||
@@ -200,6 +180,141 @@ fun MainScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AndroidScreen(settingsState: SettingsViewState) {
|
||||
BoxWithConstraints {
|
||||
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
|
||||
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
|
||||
Box(
|
||||
Modifier
|
||||
.graphicsLayer {
|
||||
translationX = -offset.value.dp.toPx()
|
||||
}
|
||||
) {
|
||||
StartPartOfScreen(settingsState)
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
val onComposed: () -> Unit = {
|
||||
scope.launch {
|
||||
offset.animateTo(
|
||||
if (chatModel.chatId.value == null) 0f else maxWidth.value,
|
||||
chatListAnimationSpec()
|
||||
)
|
||||
if (offset.value == 0f) {
|
||||
currentChatId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
if (it != null) currentChatId = it
|
||||
else onComposed()
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@{
|
||||
currentChatId?.let {
|
||||
ChatView(it, chatModel, onComposed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StartPartOfScreen(settingsState: SettingsViewState) {
|
||||
if (chatModel.setDeliveryReceipts.value) {
|
||||
SetDeliveryReceiptsView(chatModel)
|
||||
} else {
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
if (chatModel.sharedContent.value == null)
|
||||
ChatListView(chatModel, settingsState, AppLock::setPerformLA, stopped)
|
||||
else
|
||||
ShareListView(chatModel, settingsState, stopped)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CenterPartOfScreen() {
|
||||
val currentChatId by remember { ChatModel.chatId }
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { currentChatId }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
if (it != null) {
|
||||
ModalManager.center.closeModals()
|
||||
}
|
||||
}
|
||||
}
|
||||
when (val id = currentChatId) {
|
||||
null -> {
|
||||
if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colors.background),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(stringResource(MR.strings.no_selected_chat))
|
||||
}
|
||||
} else {
|
||||
ModalManager.center.showInView()
|
||||
}
|
||||
}
|
||||
else -> ChatView(id, chatModel) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EndPartOfScreen() {
|
||||
ModalManager.end.showInView()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DesktopScreen(settingsState: SettingsViewState) {
|
||||
Box {
|
||||
// 56.dp is a size of unused space of settings drawer
|
||||
Box(Modifier.width(DEFAULT_START_MODAL_WIDTH + 56.dp)) {
|
||||
StartPartOfScreen(settingsState)
|
||||
}
|
||||
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) {
|
||||
ModalManager.start.showInView()
|
||||
}
|
||||
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) {
|
||||
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
|
||||
CenterPartOfScreen()
|
||||
}
|
||||
if (ModalManager.end.hasModalsOpen()) {
|
||||
VerticalDivider()
|
||||
}
|
||||
Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH).clipToBounds()) {
|
||||
EndPartOfScreen()
|
||||
}
|
||||
}
|
||||
val (userPickerState, scaffoldState, switchingUsers ) = settingsState
|
||||
val scope = rememberCoroutineScope()
|
||||
if (scaffoldState.drawerState.isOpen) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = DEFAULT_START_MODAL_WIDTH)
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = {
|
||||
ModalManager.start.closeModals()
|
||||
scope.launch { settingsState.scaffoldState.drawerState.close() }
|
||||
})
|
||||
)
|
||||
}
|
||||
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH))
|
||||
UserPicker(chatModel, userPickerState, switchingUsers) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
}
|
||||
ModalManager.fullscreen.showInView()
|
||||
ModalManager.fullscreen.showPasscodeInView()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InitializationView() {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
|
||||
@@ -6,8 +6,7 @@ import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.Log
|
||||
import chat.simplex.common.platform.TAG
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.localauth.SetAppPasscodeView
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -41,17 +40,25 @@ object AppLock {
|
||||
text = generalGetString(MR.strings.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled),
|
||||
confirmText = generalGetString(MR.strings.la_notice_turn_on),
|
||||
onConfirm = {
|
||||
laNoticeShown.set(true)
|
||||
withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager
|
||||
showChooseLAMode(laNoticeShown)
|
||||
if (appPlatform.isAndroid) {
|
||||
showChooseLAMode()
|
||||
} else {
|
||||
AlertManager.shared.hideAlert()
|
||||
setPasscode()
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = {
|
||||
AlertManager.shared.hideAlert()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>) {
|
||||
private fun showChooseLAMode() {
|
||||
Log.d(TAG, "showLANotice")
|
||||
laNoticeShown.set(true)
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.la_lock_mode),
|
||||
text = null,
|
||||
@@ -71,7 +78,7 @@ object AppLock {
|
||||
private fun initialEnableLA() {
|
||||
val m = ChatModel
|
||||
val appPrefs = ChatController.appPrefs
|
||||
appPrefs.laMode.set(LAMode.SYSTEM)
|
||||
appPrefs.laMode.set(LAMode.default)
|
||||
authenticate(
|
||||
generalGetString(MR.strings.auth_enable_simplex_lock),
|
||||
generalGetString(MR.strings.auth_confirm_credential),
|
||||
@@ -100,7 +107,7 @@ object AppLock {
|
||||
|
||||
private fun setPasscode() {
|
||||
val appPrefs = ChatController.appPrefs
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
|
||||
@@ -130,10 +130,11 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun hasChat(id: String): Boolean = chats.firstOrNull { it.id == id } != null
|
||||
fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id }
|
||||
fun getContactChat(contactId: Long): Chat? = chats.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
|
||||
private fun getChatIndex(id: String): Int = chats.indexOfFirst { it.id == id }
|
||||
// toList() here is to prevent ConcurrentModificationException that is rarely happens but happens
|
||||
fun hasChat(id: String): Boolean = chats.toList().firstOrNull { it.id == id } != null
|
||||
fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id }
|
||||
fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
|
||||
private fun getChatIndex(id: String): Int = chats.toList().indexOfFirst { it.id == id }
|
||||
fun addChat(chat: Chat) = chats.add(index = 0, chat)
|
||||
|
||||
fun updateChatInfo(cInfo: ChatInfo) {
|
||||
@@ -437,7 +438,7 @@ object ChatModel {
|
||||
val info = getChat(id)?.chatInfo as? ChatInfo.ContactConnection ?: return
|
||||
if (info.contactConnection.connReqInv == connReqInv.value) {
|
||||
connReqInv.value = null
|
||||
ModalManager.shared.closeModals()
|
||||
ModalManager.center.closeModals()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class AppPreferences {
|
||||
set = fun(action: CallOnLockScreen) { _callOnLockScreen.set(action.name) }
|
||||
)
|
||||
val performLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false)
|
||||
val laMode = mkEnumPreference(SHARED_PREFS_LA_MODE, LAMode.SYSTEM) { LAMode.values().firstOrNull { it.name == this } }
|
||||
val laMode = mkEnumPreference(SHARED_PREFS_LA_MODE, LAMode.default) { LAMode.values().firstOrNull { it.name == this } }
|
||||
val laLockDelay = mkIntPreference(SHARED_PREFS_LA_LOCK_DELAY, 30)
|
||||
val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
|
||||
val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null)
|
||||
@@ -1882,7 +1882,7 @@ sealed class CC {
|
||||
is SetAllContactReceipts -> "/set receipts all ${onOff(enable)}"
|
||||
is ApiSetUserContactReceipts -> {
|
||||
val mrs = userMsgReceiptSettings
|
||||
"/_set receipts $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}"
|
||||
"/_set receipts contacts $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}"
|
||||
}
|
||||
is ApiHideUser -> "/_hide user $userId ${json.encodeToString(viewPwd)}"
|
||||
is ApiUnhideUser -> "/_unhide user $userId ${json.encodeToString(viewPwd)}"
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.sd.lib.compose.wheel_picker
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import chat.simplex.common.ui.theme.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
|
||||
@@ -10,6 +10,9 @@ enum class AppPlatform {
|
||||
|
||||
val isAndroid: Boolean
|
||||
get() = this == ANDROID
|
||||
|
||||
val isDesktop: Boolean
|
||||
get() = this == DESKTOP
|
||||
}
|
||||
|
||||
expect val appPlatform: AppPlatform
|
||||
@@ -19,8 +22,6 @@ val appVersionInfo: Pair<String, Int?> = if (appPlatform == AppPlatform.ANDROID)
|
||||
else
|
||||
BuildConfigCommon.DESKTOP_VERSION_NAME to null
|
||||
|
||||
expect fun initHaskell()
|
||||
|
||||
class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() {
|
||||
override fun add(element: E): Boolean {
|
||||
if(size > capacity) removeFirst()
|
||||
|
||||
@@ -28,4 +28,6 @@ expect fun screenOrientation(): ScreenOrientation
|
||||
@Composable
|
||||
expect fun screenWidth(): Dp
|
||||
|
||||
expect fun desktopExpandWindowToWidth(width: Dp)
|
||||
|
||||
expect fun isRtl(text: CharSequence): Boolean
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package chat.simplex.common.ui.theme
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -191,6 +190,11 @@ val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
|
||||
val DEFAULT_BOTTOM_PADDING = 48.dp
|
||||
val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp
|
||||
|
||||
val DEFAULT_START_MODAL_WIDTH = 388.dp
|
||||
val DEFAULT_MIN_CENTER_MODAL_WIDTH = 590.dp
|
||||
val DEFAULT_END_MODAL_WIDTH = 388.dp
|
||||
val DEFAULT_MAX_IMAGE_WIDTH = 500.dp
|
||||
|
||||
val DarkColorPalette = darkColors(
|
||||
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
|
||||
primaryVariant = SimplexBlue,
|
||||
@@ -255,6 +259,15 @@ val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow
|
||||
@Composable
|
||||
fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight
|
||||
|
||||
expect fun isSystemInDarkTheme(): Boolean
|
||||
|
||||
fun reactOnDarkThemeChanges(isDark: Boolean) {
|
||||
if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == isDark) {
|
||||
// Change active colors from light to dark and back based on system theme
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, isDark)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
|
||||
LaunchedEffect(darkTheme) {
|
||||
@@ -264,10 +277,7 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
|
||||
}
|
||||
val systemDark = isSystemInDarkTheme()
|
||||
LaunchedEffect(systemDark) {
|
||||
if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == systemDark) {
|
||||
// Change active colors from light to dark and back based on system theme
|
||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, systemDark)
|
||||
}
|
||||
reactOnDarkThemeChanges(systemDark)
|
||||
}
|
||||
val theme by CurrentColors.collectAsState()
|
||||
MaterialTheme(
|
||||
|
||||
@@ -24,6 +24,12 @@ import chat.simplex.common.platform.*
|
||||
@Composable
|
||||
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
|
||||
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
|
||||
val close = {
|
||||
close()
|
||||
if (appPlatform.isDesktop) {
|
||||
ModalManager.center.closeModals()
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = {
|
||||
close()
|
||||
})
|
||||
@@ -126,7 +132,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
ModalManager.shared.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
|
||||
ModalManager.start.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) {
|
||||
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package chat.simplex.common.views.call
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.withApi
|
||||
import chat.simplex.common.views.usersettings.showInDevelopingAlert
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@@ -24,6 +25,9 @@ class CallManager(val chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
fun acceptIncomingCall(invitation: RcvCallInvitation) {
|
||||
if (appPlatform.isDesktop) {
|
||||
return showInDevelopingAlert()
|
||||
}
|
||||
val call = chatModel.activeCall.value
|
||||
if (call == null) {
|
||||
justAcceptIncomingCall(invitation = invitation)
|
||||
|
||||
@@ -81,7 +81,7 @@ fun ChatInfoView(
|
||||
setContactAlias(chat.chatInfo.apiId, it, chatModel)
|
||||
},
|
||||
openPreferences = {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalManager.end.showCustomModal { close ->
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
ContactPreferencesView(chatModel, user, contact.contactId, close)
|
||||
@@ -136,7 +136,7 @@ fun ChatInfoView(
|
||||
})
|
||||
},
|
||||
verifyClicked = {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ModalManager.end.showModalCloseable { close ->
|
||||
remember { derivedStateOf { (chatModel.getContactChat(contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
|
||||
VerifyCodeView(
|
||||
ct.displayName,
|
||||
|
||||
@@ -70,6 +70,7 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||
Box(
|
||||
Modifier.clip(RoundedCornerShape(18.dp)).background(itemColor).padding(bottom = 3.dp)
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = {})
|
||||
.onRightClick { showMenu.value = true }
|
||||
) {
|
||||
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
|
||||
TextBubble(text, ciVersion.formattedText, sender = null, showMenu)
|
||||
@@ -116,6 +117,7 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||
Box(
|
||||
Modifier.clip(RoundedCornerShape(18.dp)).background(quoteColor).padding(bottom = 3.dp)
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = {})
|
||||
.onRightClick { showMenu.value = true }
|
||||
) {
|
||||
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
|
||||
TextBubble(text, qi.formattedText, sender = qi.sender(null), showMenu)
|
||||
|
||||
@@ -34,6 +34,7 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.platform.AudioPlayer
|
||||
import chat.simplex.common.views.usersettings.showInDevelopingAlert
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
@@ -139,7 +140,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
|
||||
val (_, code) = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
remember { derivedStateOf { (chatModel.getContactChat(chat.chatInfo.apiId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
|
||||
ChatInfoView(chatModel, ct, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
}
|
||||
@@ -149,7 +151,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
val link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
|
||||
var groupLink = link?.first
|
||||
var groupLinkMemberRole = link?.second
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
GroupChatInfoView(chatModel, groupLink, groupLinkMemberRole, {
|
||||
groupLink = it.first;
|
||||
groupLinkMemberRole = it.second
|
||||
@@ -174,7 +177,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
member to null
|
||||
}
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
|
||||
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, close, close)
|
||||
}
|
||||
@@ -233,7 +237,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
joinGroup = { groupId ->
|
||||
withApi { chatModel.controller.apiJoinGroup(groupId) }
|
||||
},
|
||||
startCall = { media ->
|
||||
startCall = out@ { media ->
|
||||
if (appPlatform.isDesktop) {
|
||||
return@out showInDevelopingAlert()
|
||||
}
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
chatModel.activeCall.value = Call(contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media)
|
||||
@@ -314,7 +321,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
withApi {
|
||||
val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id)
|
||||
if (ciInfo != null) {
|
||||
ModalManager.shared.showModal(endButtons = { ShareButton {
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModal(endButtons = { ShareButton {
|
||||
clipboard.shareText(itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||
} }) {
|
||||
ChatItemInfoView(cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
||||
@@ -326,8 +334,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(groupInfo, false, chatModel, close)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(groupInfo, false, chatModel, close)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -462,7 +471,9 @@ fun ChatInfoToolbar(
|
||||
showSearch = false
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = onBackClicked)
|
||||
if (appPlatform.isAndroid) {
|
||||
BackHandler(onBack = onBackClicked)
|
||||
}
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val menuItems = arrayListOf<@Composable () -> Unit>()
|
||||
menuItems.add {
|
||||
@@ -520,7 +531,7 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
|
||||
DefaultTopAppBar(
|
||||
navigationButton = { NavigationButtonBack(onBackClicked) },
|
||||
navigationButton = { if (appPlatform.isAndroid || showSearch) { NavigationButtonBack(onBackClicked) } },
|
||||
title = { ChatInfoToolbarTitle(chat.chatInfo) },
|
||||
onTitleClick = info,
|
||||
showSearch = showSearch,
|
||||
|
||||
@@ -170,7 +170,7 @@ fun ComposeView(
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
val maxFileSize = getMaxFileSize(FileProtocol.XFTP)
|
||||
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
|
||||
val textStyle = remember { mutableStateOf(smallFont) }
|
||||
val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) }
|
||||
val processPickedMedia = { uris: List<URI>, text: String? ->
|
||||
val content = ArrayList<UploadContent>()
|
||||
val imagesPreview = ArrayList<String>()
|
||||
@@ -655,7 +655,11 @@ fun ComposeView(
|
||||
} else {
|
||||
showChooseAttachment
|
||||
}
|
||||
IconButton(attachmentClicked, enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value) {
|
||||
IconButton(
|
||||
attachmentClicked,
|
||||
Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else 7.dp),
|
||||
enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_attach_file_filled_500),
|
||||
contentDescription = stringResource(MR.strings.attach),
|
||||
|
||||
@@ -24,6 +24,7 @@ import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.usersettings.showInDevelopingAlert
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -83,7 +84,7 @@ fun SendMsgView(
|
||||
if (showDeleteTextButton.value) {
|
||||
DeleteTextButton(composeState)
|
||||
}
|
||||
Box(Modifier.align(Alignment.BottomEnd)) {
|
||||
Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else 5.dp)) {
|
||||
val sendButtonSize = remember { Animatable(36f) }
|
||||
val sendButtonAlpha = remember { Animatable(1f) }
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -320,7 +321,10 @@ private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNex
|
||||
LockToCurrentOrientationUntilDispose()
|
||||
StopRecordButton(stopRecordingAndAddAudio)
|
||||
} else {
|
||||
val startRecording: () -> Unit = {
|
||||
val startRecording: () -> Unit = out@ {
|
||||
if (appPlatform.isDesktop) {
|
||||
return@out showInDevelopingAlert()
|
||||
}
|
||||
recState.value = RecordingState.Started(
|
||||
filePath = rec.start { progress: Int?, finished: Boolean ->
|
||||
val state = recState.value
|
||||
@@ -446,7 +450,8 @@ private fun SendMsgButton(
|
||||
role = Role.Button,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp)
|
||||
),
|
||||
)
|
||||
.onRightClick { onLongClick?.invoke() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
|
||||
@@ -113,7 +113,7 @@ private fun VerifyCodeLayout(
|
||||
} else {
|
||||
if (appPlatform.isAndroid) {
|
||||
SimpleButton(generalGetString(MR.strings.scan_code), painterResource(MR.images.ic_qr_code)) {
|
||||
ModalManager.shared.showModal {
|
||||
ModalManager.end.showModal {
|
||||
ScanCodeView(verifyCode) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
|
||||
allowModifyMembers = allowModifyMembers,
|
||||
searchText,
|
||||
openPreferences = {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalManager.end.showCustomModal { close ->
|
||||
GroupPreferencesView(chatModel, groupInfo.id, close)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -54,7 +54,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
|
||||
addMembers = {
|
||||
withApi {
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(groupInfo, false, chatModel, close)
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
ModalManager.shared.showModalCloseable(true) { closeCurrent ->
|
||||
ModalManager.end.showModalCloseable(true) { closeCurrent ->
|
||||
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
|
||||
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, closeCurrent) {
|
||||
closeCurrent()
|
||||
@@ -84,13 +84,13 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
|
||||
}
|
||||
},
|
||||
editGroupProfile = {
|
||||
ModalManager.shared.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) }
|
||||
ModalManager.end.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) }
|
||||
},
|
||||
addOrEditWelcomeMessage = {
|
||||
ModalManager.shared.showCustomModal { close -> GroupWelcomeView(chatModel, groupInfo, close) }
|
||||
ModalManager.end.showCustomModal { close -> GroupWelcomeView(chatModel, groupInfo, close) }
|
||||
},
|
||||
openPreferences = {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalManager.end.showCustomModal { close ->
|
||||
GroupPreferencesView(
|
||||
chatModel,
|
||||
chat.id,
|
||||
@@ -102,7 +102,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
|
||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
|
||||
manageGroupLink = {
|
||||
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
|
||||
ModalManager.end.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ fun GroupMemberInfoView(
|
||||
})
|
||||
},
|
||||
verifyClicked = {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ModalManager.end.showModalCloseable { close ->
|
||||
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
|
||||
VerifyCodeView(
|
||||
mem.displayName,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
@@ -20,6 +19,7 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import java.io.File
|
||||
@@ -85,7 +85,7 @@ fun CIImageView(
|
||||
@Composable
|
||||
fun imageViewFullWidth(): Dp {
|
||||
val approximatePadding = 100.dp
|
||||
return with(LocalDensity.current) { minOf(1000.dp, LocalWindowWidth() - approximatePadding) }
|
||||
return with(LocalDensity.current) { minOf(DEFAULT_MAX_IMAGE_WIDTH, LocalWindowWidth() - approximatePadding) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -93,14 +93,15 @@ fun CIImageView(
|
||||
Image(
|
||||
imageBitmap,
|
||||
contentDescription = stringResource(MR.strings.image_descr),
|
||||
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
|
||||
// .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView
|
||||
// if text is short and take all available width if text is long
|
||||
modifier = Modifier
|
||||
.width(if (imageBitmap.width * 0.97 <= imageBitmap.height) imageViewFullWidth() * 0.75f else 1000.dp)
|
||||
.width(if (imageBitmap.width * 0.97 <= imageBitmap.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH)
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = onClick
|
||||
),
|
||||
)
|
||||
.onRightClick { showMenu.value = true },
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
@@ -110,14 +111,15 @@ fun CIImageView(
|
||||
Image(
|
||||
painter,
|
||||
contentDescription = stringResource(MR.strings.image_descr),
|
||||
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
|
||||
// .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView
|
||||
// if text is short and take all available width if text is long
|
||||
modifier = Modifier
|
||||
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else 1000.dp)
|
||||
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH)
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = onClick
|
||||
),
|
||||
)
|
||||
.onRightClick { showMenu.value = true },
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ import chat.simplex.res.MR
|
||||
@Composable
|
||||
fun CIInvalidJSONView(json: String) {
|
||||
Row(Modifier
|
||||
.clickable { ModalManager.shared.showModal(true) { InvalidJSONView(json) } }
|
||||
.clickable {
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.center.showModal(true) { InvalidJSONView(json) }
|
||||
}
|
||||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||
) {
|
||||
Text(stringResource(MR.strings.invalid_data), color = Color.Red, fontStyle = FontStyle.Italic)
|
||||
|
||||
@@ -44,7 +44,7 @@ fun CIVideoView(
|
||||
val view = LocalMultiplatformView()
|
||||
VideoView(uri, file, preview, duration * 1000L, showMenu, onClick = {
|
||||
hideKeyboard(view)
|
||||
ModalManager.shared.showCustomModal(animated = false) { close ->
|
||||
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
|
||||
ImageFullScreenView(imageProvider, close)
|
||||
}
|
||||
})
|
||||
@@ -113,7 +113,7 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
|
||||
}
|
||||
Box {
|
||||
val windowWidth = LocalWindowWidth()
|
||||
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp }
|
||||
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
|
||||
PlayerView(
|
||||
player,
|
||||
width,
|
||||
@@ -142,7 +142,8 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit,
|
||||
Box(
|
||||
Modifier
|
||||
.defaultMinSize(minWidth = 40.dp, minHeight = 40.dp)
|
||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick),
|
||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||
.onRightClick { onLongClick.invoke() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
@@ -202,7 +203,7 @@ private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, durat
|
||||
@Composable
|
||||
private fun ImageView(preview: ImageBitmap, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
|
||||
val windowWidth = LocalWindowWidth()
|
||||
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp }
|
||||
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH }
|
||||
Image(
|
||||
preview,
|
||||
contentDescription = stringResource(MR.strings.video_descr),
|
||||
@@ -211,7 +212,8 @@ private fun ImageView(preview: ImageBitmap, showMenu: MutableState<Boolean>, onC
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = onClick
|
||||
),
|
||||
)
|
||||
.onRightClick { showMenu.value = true },
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
@@ -311,5 +313,5 @@ private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) {
|
||||
|
||||
private fun videoViewFullWidth(windowWidth: Dp): Dp {
|
||||
val approximatePadding = 100.dp
|
||||
return minOf(1000.dp, windowWidth - approximatePadding)
|
||||
return minOf(DEFAULT_MAX_IMAGE_WIDTH, windowWidth - approximatePadding)
|
||||
}
|
||||
|
||||
@@ -222,7 +222,8 @@ private fun PlayPauseButton(
|
||||
.combinedClickable(
|
||||
onClick = { if (!audioPlaying) play() else pause() },
|
||||
onLongClick = longClick
|
||||
),
|
||||
)
|
||||
.onRightClick { longClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
|
||||
@@ -111,7 +111,8 @@ fun ChatItemView(
|
||||
Column(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick),
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick)
|
||||
.onRightClick { showMenu.value = true },
|
||||
) {
|
||||
@Composable
|
||||
fun framedItemView() {
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.*
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
@@ -23,10 +22,8 @@ import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.base64ToBitmap
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.math.min
|
||||
|
||||
@Composable
|
||||
@@ -110,6 +107,7 @@ fun FramedItemView(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = { scrollToItem(qi.itemId?: return@combinedClickable) }
|
||||
)
|
||||
.onRightClick { showMenu.value = true }
|
||||
) {
|
||||
when (qi.content) {
|
||||
is MsgContent.MCImage -> {
|
||||
|
||||
@@ -24,7 +24,7 @@ import chat.simplex.common.views.chat.item.InvalidJSONView
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.ContactConnectionInfoView
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.platform.ntfManager
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -73,7 +73,9 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ContactConnectionView(chat.chatInfo.contactConnection) },
|
||||
click = {
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close ->
|
||||
ContactConnectionInfoView(chatModel, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close)
|
||||
}
|
||||
},
|
||||
@@ -87,7 +89,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
InvalidDataView()
|
||||
},
|
||||
click = {
|
||||
ModalManager.shared.showModal(true) { InvalidJSONView(chat.chatInfo.json) }
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.center.showModal(true) { InvalidJSONView(chat.chatInfo.json) }
|
||||
},
|
||||
dropdownMenuItems = null,
|
||||
showMenu,
|
||||
@@ -342,7 +345,9 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
|
||||
stringResource(MR.strings.set_contact_name),
|
||||
painterResource(MR.images.ic_edit),
|
||||
onClick = {
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close ->
|
||||
ContactConnectionInfoView(chatModel, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close)
|
||||
}
|
||||
showMenu.value = false
|
||||
@@ -613,7 +618,9 @@ fun ChatListNavLinkLayout(
|
||||
stopped: Boolean
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
if (!stopped) modifier = modifier.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
|
||||
if (!stopped) modifier = modifier
|
||||
.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
|
||||
.onRightClick { showMenu.value = true }
|
||||
Box(modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -8,16 +8,16 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.SettingsViewState
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -28,15 +28,13 @@ import chat.simplex.common.views.usersettings.simplexTeamUri
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||
fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val showNewChatSheet = {
|
||||
newChatSheetState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
@@ -47,7 +45,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
LaunchedEffect(Unit) {
|
||||
if (shouldShowWhatsNew(chatModel)) {
|
||||
delay(1000L)
|
||||
ModalManager.shared.showCustomModal { close -> WhatsNewView(close = close) }
|
||||
ModalManager.center.showCustomModal { close -> WhatsNewView(close = close) }
|
||||
}
|
||||
}
|
||||
LaunchedEffect(chatModel.clearOverlays.value) {
|
||||
@@ -60,13 +58,13 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
connectIfOpenedViaUri(url, chatModel)
|
||||
}
|
||||
}
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val switchingUsers = rememberSaveable { mutableStateOf(false) }
|
||||
Scaffold(topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } },
|
||||
val (userPickerState, scaffoldState, switchingUsers ) = settingsState
|
||||
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = { SettingsView(chatModel, setPerformLA) },
|
||||
drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) },
|
||||
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
|
||||
floatingActionButton = {
|
||||
if (searchInList.isEmpty()) {
|
||||
@@ -76,7 +74,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet()
|
||||
}
|
||||
},
|
||||
Modifier.padding(end = DEFAULT_PADDING - 16.dp, bottom = DEFAULT_PADDING - 16.dp),
|
||||
Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 0.dp,
|
||||
pressedElevation = 0.dp,
|
||||
@@ -91,7 +89,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(Modifier.padding(it)) {
|
||||
Box(Modifier.padding(it).padding(end = endPadding)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -112,8 +110,10 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
if (searchInList.isEmpty()) {
|
||||
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
||||
}
|
||||
UserPicker(chatModel, userPickerState, switchingUsers) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
if (appPlatform.isAndroid) {
|
||||
UserPicker(chatModel, userPickerState, switchingUsers) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
}
|
||||
}
|
||||
if (switchingUsers.value) {
|
||||
Box(
|
||||
@@ -344,7 +344,11 @@ private fun ChatList(chatModel: ChatModel, search: String) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value
|
||||
val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search) } }
|
||||
val allChats = remember { chatModel.chats }
|
||||
// In some not always reproducible situations this code produce IndexOutOfBoundsException on Compose's side
|
||||
// which is related to [derivedStateOf]. Using safe alternative instead
|
||||
// val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } }
|
||||
val chats = filteredChats(showUnreadAndFavorites, search, allChats.toList())
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
listState
|
||||
@@ -360,13 +364,12 @@ private fun ChatList(chatModel: ChatModel, search: String) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun filteredChats(showUnreadAndFavorites: Boolean, searchText: String): List<Chat> {
|
||||
val chatModel = ChatModel
|
||||
private fun filteredChats(showUnreadAndFavorites: Boolean, searchText: String, chats: List<Chat>): List<Chat> {
|
||||
val s = searchText.trim().lowercase()
|
||||
return if (s.isEmpty() && !showUnreadAndFavorites)
|
||||
chatModel.chats
|
||||
chats
|
||||
else {
|
||||
chatModel.chats.filter { chat ->
|
||||
chats.filter { chat ->
|
||||
when (val cInfo = chat.chatInfo) {
|
||||
is ChatInfo.Direct -> if (s.isEmpty()) {
|
||||
filtered(chat)
|
||||
|
||||
@@ -13,20 +13,24 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.SettingsViewState
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.Chat
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.BackHandler
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||
fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) {
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val switchingUsers = rememberSaveable { mutableStateOf(false) }
|
||||
val (userPickerState, scaffoldState, switchingUsers) = settingsState
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
Scaffold(
|
||||
Modifier.padding(end = endPadding),
|
||||
scaffoldState = scaffoldState,
|
||||
topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||
) {
|
||||
Box(Modifier.padding(it)) {
|
||||
@@ -42,9 +46,11 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||
}
|
||||
}
|
||||
}
|
||||
UserPicker(chatModel, userPickerState, switchingUsers, showSettings = false, showCancel = true, cancelClicked = {
|
||||
chatModel.sharedContent.value = null
|
||||
})
|
||||
if (appPlatform.isAndroid) {
|
||||
UserPicker(chatModel, userPickerState, switchingUsers, showSettings = false, showCancel = true, cancelClicked = {
|
||||
chatModel.sharedContent.value = null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -41,6 +41,11 @@ fun UserPicker(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var newChat by remember { mutableStateOf(userPickerState.value) }
|
||||
if (newChat.isVisible()) {
|
||||
BackHandler {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
}
|
||||
}
|
||||
val users by remember {
|
||||
derivedStateOf {
|
||||
chatModel.users
|
||||
@@ -121,6 +126,7 @@ fun UserPicker(
|
||||
delay(500)
|
||||
switchingUsers.value = true
|
||||
}
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
chatModel.controller.changeActiveUser(u.user.userId, null)
|
||||
job.cancel()
|
||||
switchingUsers.value = false
|
||||
@@ -159,6 +165,7 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = if (!u.activeUser) LocalIndication.current else null
|
||||
)
|
||||
.onRightClick { onLongClick() }
|
||||
.padding(padding),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
||||
@@ -83,7 +83,7 @@ private fun deleteArchiveAlert(m: ChatModel, archivePath: String) {
|
||||
if (fileDeleted) {
|
||||
m.controller.appPrefs.chatArchiveName.set(null)
|
||||
m.controller.appPrefs.chatArchiveTime.set(null)
|
||||
ModalManager.shared.closeModal()
|
||||
ModalManager.start.closeModal()
|
||||
} else {
|
||||
Log.e(TAG, "deleteArchiveAlert delete() error")
|
||||
}
|
||||
|
||||
@@ -357,11 +357,11 @@ private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastSta
|
||||
}
|
||||
if (m.chatDbStatus.value !is DBMigrationResult.OK) {
|
||||
/** Hide current view and show [DatabaseErrorView] */
|
||||
ModalManager.shared.closeModals()
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
return@withApi
|
||||
}
|
||||
if (m.currentUser.value == null) {
|
||||
ModalManager.shared.closeModals()
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
return@withApi
|
||||
} else {
|
||||
m.controller.apiStartChat()
|
||||
|
||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -32,7 +32,11 @@ fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> U
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
NavigationButtonBack(onButtonClicked = close)
|
||||
if (showClose) {
|
||||
NavigationButtonBack(onButtonClicked = close)
|
||||
} else {
|
||||
Spacer(Modifier)
|
||||
}
|
||||
Row {
|
||||
endButtons()
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
expect fun Modifier.onRightClick(action: () -> Unit): Modifier
|
||||
|
||||
expect interface DefaultExposedDropdownMenuBoxScope {
|
||||
@Composable
|
||||
open fun DefaultExposedDropdownMenu(
|
||||
|
||||
@@ -45,7 +45,7 @@ fun authenticateWithPasscode(
|
||||
completed: (LAResult) -> Unit
|
||||
) {
|
||||
val password = DatabaseUtils.ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(MR.strings.la_no_app_password)))
|
||||
ModalManager.shared.showPasscodeCustomModal { close ->
|
||||
ModalManager.fullscreen.showPasscodeCustomModal { close ->
|
||||
BackHandler {
|
||||
close()
|
||||
completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled)))
|
||||
|
||||
@@ -8,43 +8,51 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.themedBackground
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@Composable
|
||||
fun ModalView(
|
||||
close: () -> Unit,
|
||||
showClose: Boolean = true,
|
||||
background: Color = MaterialTheme.colors.background,
|
||||
modifier: Modifier = Modifier,
|
||||
endButtons: @Composable RowScope.() -> Unit = {},
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
if (showClose) {
|
||||
BackHandler(onBack = close)
|
||||
}
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
|
||||
CloseSheetBar(close, endButtons)
|
||||
CloseSheetBar(close, showClose, endButtons)
|
||||
Box(modifier) { content() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ModalManager {
|
||||
enum class ModalPlacement {
|
||||
START, CENTER, END, FULLSCREEN
|
||||
}
|
||||
|
||||
class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
private val modalViews = arrayListOf<Pair<Boolean, (@Composable (close: () -> Unit) -> Unit)>>()
|
||||
private val modalCount = mutableStateOf(0)
|
||||
private val toRemove = mutableSetOf<Int>()
|
||||
private var oldViewChanging = AtomicBoolean(false)
|
||||
private var passcodeView: MutableState<(@Composable (close: () -> Unit) -> Unit)?> = mutableStateOf(null)
|
||||
|
||||
fun showModal(settings: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) {
|
||||
fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) {
|
||||
showCustomModal { close ->
|
||||
ModalView(close, endButtons = endButtons, content = content)
|
||||
ModalView(close, showClose = showClose, endButtons = endButtons, content = content)
|
||||
}
|
||||
}
|
||||
|
||||
fun showModalCloseable(settings: Boolean = false, content: @Composable (close: () -> Unit) -> Unit) {
|
||||
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable (close: () -> Unit) -> Unit) {
|
||||
showCustomModal { close ->
|
||||
ModalView(close, content = { content(close) })
|
||||
ModalView(close, showClose = showClose, content = { content(close) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +63,17 @@ class ModalManager {
|
||||
if (toRemove.isNotEmpty()) {
|
||||
runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } }
|
||||
}
|
||||
modalViews.add(animated to modal)
|
||||
// Make animated appearance only on Android (everytime) and on Desktop (when it's on the start part of the screen or modals > 0)
|
||||
// to prevent unneeded animation on different situations
|
||||
val anim = if (appPlatform.isAndroid) animated else animated && (modalCount.value > 0 || placement == ModalPlacement.START)
|
||||
modalViews.add(anim to modal)
|
||||
modalCount.value = modalViews.size - toRemove.size
|
||||
|
||||
if (placement == ModalPlacement.CENTER) {
|
||||
ChatModel.chatId.value = null
|
||||
} else if (placement == ModalPlacement.END) {
|
||||
desktopExpandWindowToWidth(DEFAULT_START_MODAL_WIDTH + DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH)
|
||||
}
|
||||
}
|
||||
|
||||
fun showPasscodeCustomModal(modal: @Composable (close: () -> Unit) -> Unit) {
|
||||
@@ -146,6 +163,17 @@ private fun <T> animationSpec() = tween<T>(durationMillis = 250, easing = FastOu
|
||||
// private fun <T> animationSpecFromEnd() = tween<T>(durationMillis = 100, easing = FastOutSlowInEasing)
|
||||
|
||||
companion object {
|
||||
val shared = ModalManager()
|
||||
private val shared = ModalManager()
|
||||
val start = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.START)
|
||||
val center = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.CENTER)
|
||||
val end = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.END)
|
||||
val fullscreen = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.FULLSCREEN)
|
||||
|
||||
fun closeAllModalsEverywhere() {
|
||||
start.closeModals()
|
||||
center.closeModals()
|
||||
end.closeModals()
|
||||
fullscreen.closeModals()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.*
|
||||
@@ -15,8 +14,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.platform.screenWidth
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.ValueTitleDesc
|
||||
import chat.simplex.common.views.helpers.ValueTitle
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@@ -132,7 +130,9 @@ fun SectionItemViewSpaceBetween(
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = minHeight)
|
||||
Row(
|
||||
if (click == null || disabled) modifier.padding(padding) else modifier.combinedClickable(onClick = click, onLongClick = onLongClick).padding(padding),
|
||||
if (click == null || disabled) modifier.padding(padding) else modifier
|
||||
.combinedClickable(onClick = click, onLongClick = onLongClick).padding(padding)
|
||||
.onRightClick { onLongClick?.invoke() },
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
@@ -70,7 +70,7 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
|
||||
if (createdUser != null) {
|
||||
m.controller.startChat(createdUser)
|
||||
}
|
||||
ModalManager.shared.closeModals()
|
||||
ModalManager.fullscreen.closeModals()
|
||||
AlertManager.shared.hideAlert()
|
||||
completed(LAResult.Success)
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -30,7 +30,7 @@ fun AddContactView(connReqInvitation: String, connIncognito: Boolean) {
|
||||
connIncognito = connIncognito,
|
||||
share = { clipboard.shareText(connReqInvitation) },
|
||||
learnMore = {
|
||||
ModalManager.shared.showModal {
|
||||
ModalManager.center.showModal {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
|
||||
@@ -45,7 +45,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
|
||||
chatModel.chatId.value = groupInfo.id
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
close.invoke()
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(groupInfo, true, chatModel, close)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ fun ContactConnectionInfoView(
|
||||
**/
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (!ModalManager.shared.hasModalsOpen()) {
|
||||
if (!ModalManager.center.hasModalsOpen()) {
|
||||
chatModel.connReqInv.value = null
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ fun ContactConnectionInfoView(
|
||||
onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) },
|
||||
share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) },
|
||||
learnMore = {
|
||||
ModalManager.shared.showModal {
|
||||
ModalManager.center.showModal {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
|
||||
@@ -35,7 +35,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
|
||||
**/
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (!ModalManager.shared.hasModalsOpen()) {
|
||||
if (!ModalManager.center.hasModalsOpen()) {
|
||||
m.connReqInv.value = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,15 +40,18 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
|
||||
stopped,
|
||||
addContact = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.shared.showModal { CreateLinkView(chatModel, CreateLinkTab.ONE_TIME) }
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModal { CreateLinkView(chatModel, CreateLinkTab.ONE_TIME) }
|
||||
},
|
||||
connectViaLink = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.shared.showModalCloseable { close -> ConnectViaLinkView(chatModel, close) }
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close -> ConnectViaLinkView(chatModel, close) }
|
||||
},
|
||||
createGroup = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.shared.showCustomModal { close -> AddGroupView(chatModel, close) }
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, close) }
|
||||
},
|
||||
closeNewChatSheet,
|
||||
)
|
||||
@@ -93,10 +96,12 @@ private fun NewChatSheetLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
val maxWidth = with(LocalDensity.current) { screenWidth() * density }
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(end = endPadding)
|
||||
.offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else 0, 0) }
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { closeNewChatSheet(true) }
|
||||
.drawBehind { drawRect(animatedColor.value) },
|
||||
|
||||
@@ -4,9 +4,11 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import boofcv.alg.drawing.FiducialImageEngine
|
||||
import boofcv.alg.fiducial.qrcode.*
|
||||
@@ -25,7 +27,7 @@ fun QRCode(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
BoxWithConstraints {
|
||||
BoxWithConstraints(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
val maxWidthInPx = with(LocalDensity.current) { maxWidth.roundToPx() }
|
||||
val qr = remember(maxWidthInPx, connReq, tintColor, withLogo) {
|
||||
qrCodeBitmap(connReq, maxWidthInPx).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
|
||||
@@ -34,7 +36,9 @@ fun QRCode(
|
||||
Image(
|
||||
bitmap = qr,
|
||||
contentDescription = stringResource(MR.strings.image_descr_qr_code),
|
||||
modifier
|
||||
Modifier
|
||||
.widthIn(max = 500.dp)
|
||||
.then(modifier)
|
||||
.clickable {
|
||||
scope.launch {
|
||||
val image = qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
|
||||
|
||||
@@ -57,8 +57,13 @@ fun CreateSimpleXAddress(m: ChatModel) {
|
||||
}
|
||||
},
|
||||
nextStep = {
|
||||
m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode)
|
||||
m.onboardingStage.value = OnboardingStage.Step4_SetNotificationsMode
|
||||
val next = if (appPlatform.isAndroid) {
|
||||
OnboardingStage.Step4_SetNotificationsMode
|
||||
} else {
|
||||
OnboardingStage.OnboardingComplete
|
||||
}
|
||||
m.controller.appPrefs.onboardingStage.set(next)
|
||||
m.onboardingStage.value = next
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = n
|
||||
|
||||
if (onboardingStage != null) {
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
|
||||
OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.shared.closeModal() })
|
||||
OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.fullscreen.closeModal() })
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
|
||||
SimpleXInfoLayout(
|
||||
user = chatModel.currentUser.value,
|
||||
onboardingStage = if (onboarding) chatModel.onboardingStage else null,
|
||||
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
|
||||
showModal = { modalView -> { if (onboarding) ModalManager.fullscreen.showModal { modalView(chatModel) } else ModalManager.start.showModal { modalView(chatModel) } } },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ fun SimpleXInfoLayout(
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(start = DEFAULT_PADDING , end = DEFAULT_PADDING, top = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Box(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 10.dp), contentAlignment = Alignment.Center) {
|
||||
SimpleXLogo()
|
||||
@@ -48,9 +49,11 @@ fun SimpleXInfoLayout(
|
||||
|
||||
Text(stringResource(MR.strings.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 48.dp).padding(horizontal = 36.dp), textAlign = TextAlign.Center)
|
||||
|
||||
InfoRow(painterResource(MR.images.privacy), MR.strings.privacy_redefined, MR.strings.first_platform_without_user_ids, width = 80.dp)
|
||||
InfoRow(painterResource(MR.images.shield), MR.strings.immune_to_spam_and_abuse, MR.strings.people_can_connect_only_via_links_you_share)
|
||||
InfoRow(painterResource(if (isInDarkTheme()) MR.images.decentralized_light else MR.images.decentralized), MR.strings.decentralized, MR.strings.opensource_protocol_and_code_anybody_can_run_servers)
|
||||
Column {
|
||||
InfoRow(painterResource(MR.images.privacy), MR.strings.privacy_redefined, MR.strings.first_platform_without_user_ids, width = 80.dp)
|
||||
InfoRow(painterResource(MR.images.shield), MR.strings.immune_to_spam_and_abuse, MR.strings.people_can_connect_only_via_links_you_share)
|
||||
InfoRow(painterResource(if (isInDarkTheme()) MR.images.decentralized_light else MR.images.decentralized), MR.strings.decentralized, MR.strings.opensource_protocol_and_code_anybody_can_run_servers)
|
||||
}
|
||||
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ object AppearanceScope {
|
||||
) {
|
||||
val currentTheme by CurrentColors.collectAsState()
|
||||
SectionView(stringResource(MR.strings.settings_section_title_themes)) {
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme()
|
||||
val state = remember { derivedStateOf { currentTheme.name } }
|
||||
ThemeSelector(state) {
|
||||
ThemeManager.applyTheme(it, darkTheme)
|
||||
@@ -226,7 +226,7 @@ object AppearanceScope {
|
||||
|
||||
@Composable
|
||||
private fun ThemeSelector(state: State<String>, onSelected: (String) -> Unit) {
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme()
|
||||
val values by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second.name to it.third }) }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.theme),
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.ui.platform.LocalUriHandler
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.common.views.TerminalView
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -41,3 +42,10 @@ fun DeveloperView(
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
fun showInDevelopingAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.in_developing_title),
|
||||
text = generalGetString(MR.strings.in_developing_desc)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ fun NotificationsSettingsView(
|
||||
notificationsMode = remember { chatModel.controller.appPrefs.notificationsMode.state },
|
||||
notificationPreviewMode = chatModel.notificationPreviewMode,
|
||||
showPage = { page ->
|
||||
ModalManager.shared.showModalCloseable(true) {
|
||||
ModalManager.start.showModalCloseable(true) {
|
||||
when (page) {
|
||||
CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.controller.appPrefs.notificationsMode.state) { changeNotificationsMode(it, chatModel) }
|
||||
CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected)
|
||||
|
||||
@@ -278,7 +278,7 @@ fun SimplexLockView(
|
||||
}
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
@@ -306,7 +306,7 @@ fun SimplexLockView(
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
LAResult.Success -> {
|
||||
if (!selfDestruct.get()) {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
EnableSelfDestruct(selfDestruct, close)
|
||||
}
|
||||
} else {
|
||||
@@ -322,7 +322,7 @@ fun SimplexLockView(
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode)) { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
@@ -345,7 +345,7 @@ fun SimplexLockView(
|
||||
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode)) { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
passcodeKeychain = ksSelfDestructPassword,
|
||||
@@ -380,7 +380,7 @@ fun SimplexLockView(
|
||||
setPerformLA(true)
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
@@ -427,7 +427,7 @@ fun SimplexLockView(
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.self_destruct_passcode).uppercase()) {
|
||||
val openInfo = {
|
||||
ModalManager.shared.showModal {
|
||||
ModalManager.start.showModal {
|
||||
SelfDestructInfoView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
|
||||
}
|
||||
|
||||
fun showServer(server: ServerCfg) {
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ModalManager.start.showModalCloseable(true) { close ->
|
||||
var old by remember { mutableStateOf(server) }
|
||||
val index = servers.indexOf(old)
|
||||
ProtocolServerView(
|
||||
@@ -117,7 +117,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
|
||||
if (appPlatform.isAndroid) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
ScanProtocolServer {
|
||||
close()
|
||||
servers = servers + it
|
||||
|
||||
@@ -11,10 +11,10 @@ import androidx.compose.ui.Modifier
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.res.MR
|
||||
import chat.simplex.common.platform.TAG
|
||||
import chat.simplex.common.platform.Log
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.views.helpers.*
|
||||
|
||||
@@ -73,8 +73,9 @@ private fun SetDeliveryReceiptsLayout(
|
||||
skip: () -> Unit,
|
||||
userCount: Int,
|
||||
) {
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
Column(
|
||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING),
|
||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING, end = endPadding),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.delivery_receipts_title))
|
||||
|
||||
@@ -23,16 +23,17 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.appVersionInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.database.DatabaseView
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.SimpleXInfo
|
||||
import chat.simplex.common.views.onboarding.WhatsNewView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) {
|
||||
val user = chatModel.currentUser.value
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
|
||||
@@ -48,10 +49,10 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
chatModel.controller.appPrefs.incognito,
|
||||
user.displayName,
|
||||
setPerformLA = setPerformLA,
|
||||
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
|
||||
showSettingsModal = { modalView -> { ModalManager.shared.showModal(true) { modalView(chatModel) } } },
|
||||
showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } },
|
||||
showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } },
|
||||
showSettingsModalWithSearch = { modalView ->
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
val search = rememberSaveable { mutableStateOf("") }
|
||||
ModalView(
|
||||
{ close() },
|
||||
@@ -61,12 +62,12 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
content = { modalView(chatModel, search) })
|
||||
}
|
||||
},
|
||||
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
|
||||
showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } },
|
||||
showVersion = {
|
||||
withApi {
|
||||
val info = chatModel.controller.apiGetVersion()
|
||||
if (info != null) {
|
||||
ModalManager.shared.showModal { VersionInfoView(info) }
|
||||
ModalManager.start.showModal { VersionInfoView(info) }
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -75,7 +76,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
block()
|
||||
} else {
|
||||
var autoShow = true
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ModalManager.fullscreen.showModalCloseable { close ->
|
||||
val onFinishAuth = { success: Boolean ->
|
||||
if (success) {
|
||||
close()
|
||||
@@ -104,6 +105,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
}
|
||||
}
|
||||
},
|
||||
drawerState = drawerState,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -125,15 +127,25 @@ fun SettingsLayout(
|
||||
showSettingsModalWithSearch: (@Composable (ChatModel, MutableState<String>) -> Unit) -> Unit,
|
||||
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
showVersion: () -> Unit,
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
|
||||
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit,
|
||||
drawerState: DrawerState,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val closeSettings: () -> Unit = { scope.launch { drawerState.close() } }
|
||||
if (drawerState.isOpen) {
|
||||
BackHandler {
|
||||
closeSettings()
|
||||
}
|
||||
}
|
||||
val theme = CurrentColors.collectAsState()
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Box(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).themedBackground(theme.value.base)) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = DEFAULT_PADDING)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.themedBackground(theme.value.base)
|
||||
.padding(top = if (appPlatform.isAndroid) DEFAULT_PADDING else DEFAULT_PADDING * 3)
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.your_settings))
|
||||
|
||||
@@ -178,6 +190,17 @@ fun SettingsLayout(
|
||||
SettingsSectionApp(showSettingsModal, showCustomModal, showVersion, withAuth)
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
if (appPlatform.isDesktop) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.background)
|
||||
.background(if (isInDarkTheme()) ToolbarDark else ToolbarLight)
|
||||
.padding(start = 4.dp, top = 8.dp)
|
||||
) {
|
||||
NavigationButtonBack(closeSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,6 +533,7 @@ fun PreviewSettingsLayout() {
|
||||
showCustomModal = { {} },
|
||||
showVersion = {},
|
||||
withAuth = { _, _, _ -> },
|
||||
drawerState = DrawerState(DrawerValue.Closed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ fun UserAddressView(
|
||||
}
|
||||
},
|
||||
learnMore = {
|
||||
ModalManager.shared.showModal {
|
||||
ModalManager.start.showModal {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
|
||||
@@ -30,6 +30,7 @@ import chat.simplex.common.views.database.PassphraseField
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.CreateProfile
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -47,11 +48,15 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
|
||||
showHiddenProfilesNotice = m.controller.appPrefs.showHiddenProfilesNotice,
|
||||
visibleUsersCount = visibleUsersCount(m),
|
||||
addUser = {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ModalManager.center.showModalCloseable { close ->
|
||||
CreateProfile(m, close)
|
||||
}
|
||||
},
|
||||
activateUser = { user ->
|
||||
if (appPlatform.isDesktop) {
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
withBGApi {
|
||||
m.controller.changeActiveUser(user.userId, userViewPassword(user, searchTextOrPassword.value.trim()))
|
||||
}
|
||||
@@ -99,7 +104,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
|
||||
},
|
||||
unhideUser = { user ->
|
||||
if (passwordEntryRequired(user, searchTextOrPassword.value)) {
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ModalManager.start.showModalCloseable(true) { close ->
|
||||
ProfileActionView(UserProfileAction.UNHIDE, user) { pwd ->
|
||||
withBGApi {
|
||||
setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, pwd) }
|
||||
@@ -122,7 +127,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
|
||||
withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user.userId) } }
|
||||
},
|
||||
showHiddenProfile = { user ->
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ModalManager.start.showModalCloseable(true) { close ->
|
||||
HiddenProfileView(m, user) {
|
||||
profileHidden.value = true
|
||||
withBGApi {
|
||||
@@ -328,7 +333,7 @@ private fun passwordEntryRequired(user: User, searchTextOrPassword: String): Boo
|
||||
|
||||
private fun removeUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean, searchTextOrPassword: String) {
|
||||
if (passwordEntryRequired(user, searchTextOrPassword)) {
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ModalManager.start.showModalCloseable(true) { close ->
|
||||
ProfileActionView(UserProfileAction.DELETE, user) { pwd ->
|
||||
withBGApi {
|
||||
doRemoveUser(m, user, users, delSMPQueues, pwd)
|
||||
|
||||
@@ -267,6 +267,9 @@
|
||||
<string name="you_have_no_chats">You have no chats</string>
|
||||
<string name="no_filtered_chats">No filtered chats</string>
|
||||
|
||||
<!-- ChatView.kt -->
|
||||
<string name="no_selected_chat">No selected chat</string>
|
||||
|
||||
<!-- ShareListView.kt -->
|
||||
<string name="share_message">Share message…</string>
|
||||
<string name="share_image">Share media…</string>
|
||||
@@ -1523,4 +1526,8 @@
|
||||
<string name="delivery_receipts_are_disabled">Delivery receipts are disabled!</string>
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">You can enable them later via app Privacy & Security settings.</string>
|
||||
<string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string>
|
||||
|
||||
<!-- Under development -->
|
||||
<string name="in_developing_title">Coming soon!</string>
|
||||
<string name="in_developing_desc">This feature is not yet supported. Try the next release.</string>
|
||||
</resources>
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.*
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.desktopPlatform
|
||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||
import chat.simplex.common.views.helpers.FileDialogChooser
|
||||
import kotlinx.coroutines.*
|
||||
@@ -24,7 +25,11 @@ import java.io.File
|
||||
val simplexWindowState = SimplexWindowState()
|
||||
|
||||
fun showApp() = application {
|
||||
val windowState = rememberWindowState(placement = WindowPlacement.Floating)
|
||||
// For some reason on Linux actual width will be 10.dp less after specifying it here. If we specify 1366,
|
||||
// it will show 1356. But after that we can still update it to 1366 by changing window state. Just making it +10 now here
|
||||
val width = if (desktopPlatform.isLinux()) 1376.dp else 1366.dp
|
||||
val windowState = rememberWindowState(placement = WindowPlacement.Floating, width = width, height = 768.dp)
|
||||
simplexWindowState.windowState = windowState
|
||||
Window(state = windowState, onCloseRequest = ::exitApplication, onKeyEvent = {
|
||||
if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) {
|
||||
simplexWindowState.backstack.lastOrNull()?.invoke() != null
|
||||
@@ -108,6 +113,7 @@ fun showApp() = application {
|
||||
}
|
||||
|
||||
class SimplexWindowState {
|
||||
lateinit var windowState: WindowState
|
||||
val backstack = mutableStateListOf<() -> Unit>()
|
||||
val openDialog = DialogState<File?>()
|
||||
val openMultipleDialog = DialogState<List<File>>()
|
||||
@@ -144,6 +150,3 @@ fun AppPreview() {
|
||||
AppScreen()
|
||||
}
|
||||
}
|
||||
|
||||
/** Needed for [chat.simplex.common.platform.Files] to get path to jar file */
|
||||
class DesktopApp()
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import chat.simplex.common.DesktopApp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.views.call.RcvCallInvitation
|
||||
import chat.simplex.common.views.helpers.withBGApi
|
||||
import java.io.*
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.util.*
|
||||
|
||||
actual val appPlatform = AppPlatform.DESKTOP
|
||||
@@ -39,29 +35,3 @@ private fun applyAppLocale() {
|
||||
if (lang == null || lang == Locale.getDefault().language) return
|
||||
Locale.setDefault(Locale.forLanguageTag(lang))
|
||||
}
|
||||
|
||||
@Suppress("UnsafeDynamicallyLoadedCode")
|
||||
actual fun initHaskell() {
|
||||
val libApp = "libapp-lib.${desktopPlatform.libExtension}"
|
||||
val libsTmpDir = File(tmpDir.absolutePath + File.separator + "libs")
|
||||
copyResources(desktopPlatform.libPath, libsTmpDir.toPath())
|
||||
System.load(File(libsTmpDir, libApp).absolutePath)
|
||||
libsTmpDir.deleteRecursively()
|
||||
initHS()
|
||||
}
|
||||
|
||||
private fun copyResources(from: String, to: Path) {
|
||||
val resource = DesktopApp::class.java.getResource("")!!.toURI()
|
||||
val fileSystem = FileSystems.newFileSystem(resource, emptyMap<String, String>())
|
||||
val resPath = fileSystem.getPath(from)
|
||||
Files.walkFileTree(resPath, object: SimpleFileVisitor<Path>() {
|
||||
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||
Files.createDirectories(to.resolve(resPath.relativize(dir).toString()))
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||
Files.copy(file, to.resolve(resPath.relativize(file).toString()), StandardCopyOption.REPLACE_EXISTING)
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,15 +7,6 @@ import chat.simplex.res.MR
|
||||
import java.io.*
|
||||
import java.net.URI
|
||||
|
||||
private fun applicationParentPath(): String = try {
|
||||
DesktopApp::class.java.protectionDomain!!.codeSource.location.toURI().path
|
||||
.replaceAfterLast("/", "")
|
||||
.replaceAfterLast(File.separator, "")
|
||||
.replace("/", File.separator)
|
||||
} catch (e: Exception) {
|
||||
"./"
|
||||
}
|
||||
|
||||
actual val dataDir: File = File(desktopPlatform.dataPath)
|
||||
actual val tmpDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex").also { it.deleteOnExit() }
|
||||
actual val filesDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_files")
|
||||
|
||||
@@ -14,6 +14,8 @@ enum class DesktopPlatform(val libPath: String, val libExtension: String, val co
|
||||
WINDOWS_X86_64("/libs/windows-x86_64", "dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX"),
|
||||
MAC_X86_64("/libs/mac-x86_64", "dylib", unixConfigPath, unixDataPath),
|
||||
MAC_AARCH64("/libs/mac-aarch64", "dylib", unixConfigPath, unixDataPath);
|
||||
|
||||
fun isLinux() = this == LINUX_X86_64 || this == LINUX_AARCH64
|
||||
}
|
||||
|
||||
private fun detectDesktopPlatform(): DesktopPlatform {
|
||||
|
||||
@@ -38,7 +38,7 @@ actual fun PlatformTextField(
|
||||
val cs = composeState.value
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp)
|
||||
val padding = PaddingValues(12.dp, 12.dp, 45.dp, 0.dp)
|
||||
LaunchedEffect(cs.contextItem) {
|
||||
if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect
|
||||
// In replying state
|
||||
@@ -74,14 +74,10 @@ actual fun PlatformTextField(
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 12.dp)
|
||||
.padding(top = 4.dp)
|
||||
.padding(bottom = 6.dp)
|
||||
) {
|
||||
Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
innerTextField()
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.views.usersettings.showInDevelopingAlert
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
actual class RecorderNative: RecorderInterface {
|
||||
@@ -18,7 +19,7 @@ actual class RecorderNative: RecorderInterface {
|
||||
|
||||
actual object AudioPlayer: AudioPlayerInterface {
|
||||
override fun play(filePath: String?, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, resetOnEnd: Boolean) {
|
||||
/*LALAL*/
|
||||
showInDevelopingAlert()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
|
||||
@@ -4,8 +4,8 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.simplexWindowState
|
||||
import com.russhwolf.settings.*
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.icerock.moko.resources.desc.desc
|
||||
@@ -49,6 +49,11 @@ actual fun screenWidth(): Dp {
|
||||
return width*/
|
||||
}// LALAL java.awt.Desktop.getDesktop()
|
||||
|
||||
actual fun desktopExpandWindowToWidth(width: Dp) {
|
||||
if (simplexWindowState.windowState.size.width >= width) return
|
||||
simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = width)
|
||||
}
|
||||
|
||||
actual fun isRtl(text: CharSequence): Boolean {
|
||||
if (text.isEmpty()) return false
|
||||
return text.any { char ->
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import chat.simplex.common.views.usersettings.showInDevelopingAlert
|
||||
import java.net.URI
|
||||
|
||||
actual class VideoPlayer: VideoPlayerInterface {
|
||||
@@ -12,39 +14,40 @@ actual class VideoPlayer: VideoPlayerInterface {
|
||||
defaultPreview: ImageBitmap,
|
||||
defaultDuration: Long,
|
||||
soundEnabled: Boolean
|
||||
): VideoPlayer = VideoPlayer()
|
||||
actual fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean { TODO() }
|
||||
actual fun release(uri: URI, gallery: Boolean, remove: Boolean) { TODO() }
|
||||
): VideoPlayer = VideoPlayer().also {
|
||||
it.preview.value = defaultPreview
|
||||
it.duration.value = defaultDuration
|
||||
it.soundEnabled.value = soundEnabled
|
||||
}
|
||||
actual fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean { /*TODO*/ return false }
|
||||
actual fun release(uri: URI, gallery: Boolean, remove: Boolean) { /*TODO*/ }
|
||||
actual fun stopAll() { /*LALAL*/ }
|
||||
actual fun releaseAll() { /*LALAL*/ }
|
||||
}
|
||||
|
||||
override val soundEnabled: MutableState<Boolean>
|
||||
get() = TODO("Not yet implemented")
|
||||
override val brokenVideo: MutableState<Boolean>
|
||||
get() = TODO("Not yet implemented")
|
||||
override val videoPlaying: MutableState<Boolean>
|
||||
get() = TODO("Not yet implemented")
|
||||
override val progress: MutableState<Long>
|
||||
get() = TODO("Not yet implemented")
|
||||
override val duration: MutableState<Long>
|
||||
get() = TODO("Not yet implemented")
|
||||
override val preview: MutableState<ImageBitmap>
|
||||
get() = TODO("Not yet implemented")
|
||||
override val soundEnabled: MutableState<Boolean> = mutableStateOf(false)
|
||||
override val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
|
||||
override val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
|
||||
override val progress: MutableState<Long> = mutableStateOf(0L)
|
||||
override val duration: MutableState<Long> = mutableStateOf(0L)
|
||||
override val preview: MutableState<ImageBitmap> = mutableStateOf(ImageBitmap(0, 0))
|
||||
|
||||
override fun stop() {
|
||||
TODO("Not yet implemented")
|
||||
/*TODO*/
|
||||
}
|
||||
|
||||
override fun play(resetOnEnd: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
if (appPlatform.isDesktop) {
|
||||
showInDevelopingAlert()
|
||||
}
|
||||
}
|
||||
|
||||
override fun enableSound(enable: Boolean): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
/*TODO*/
|
||||
return false
|
||||
}
|
||||
|
||||
override fun release(remove: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
/*TODO*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package chat.simplex.common.ui.theme
|
||||
|
||||
import com.jthemedetecor.OsThemeDetector
|
||||
|
||||
private val detector: OsThemeDetector = OsThemeDetector.getDetector()
|
||||
.apply {
|
||||
registerListener(::reactOnDarkThemeChanges)
|
||||
}
|
||||
|
||||
actual fun isSystemInDarkTheme(): Boolean = detector.isDark
|
||||
@@ -20,7 +20,7 @@ actual fun SimpleAndAnimatedImageView(
|
||||
// LALAL make it animated too
|
||||
ImageView(imageBitmap.toAwtImage().toPainter()) {
|
||||
if (getLoadedFilePath(file) != null) {
|
||||
ModalManager.shared.showCustomModal(animated = false) { close ->
|
||||
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
|
||||
ImageFullScreenView(imageProvider, close)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.DropdownMenu
|
||||
@@ -9,7 +9,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.exp
|
||||
|
||||
actual fun Modifier.onRightClick(action: () -> Unit): Modifier = contextMenuOpenDetector { action() }
|
||||
|
||||
actual interface DefaultExposedDropdownMenuBoxScope {
|
||||
@Composable
|
||||
|
||||
@@ -1,22 +1,88 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Density
|
||||
import chat.simplex.common.model.CIFile
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.simplexWindowState
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
import kotlin.io.path.toPath
|
||||
|
||||
// LALAL MAKE REALLY ANNOTATED STRING FROM HTML
|
||||
actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString {
|
||||
return AnnotatedString(text)
|
||||
private val bStyle = SpanStyle(fontWeight = FontWeight.Bold)
|
||||
private val iStyle = SpanStyle(fontStyle = FontStyle.Italic)
|
||||
private fun fontStyle(color: String) =
|
||||
SpanStyle(color = Color(color.replace("#", "ff").toLongOrNull(16) ?: Color.White.toArgb().toLong()))
|
||||
|
||||
actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString = try {
|
||||
buildAnnotatedString {
|
||||
fun String.substringSafe(start: Int, len: Int): String =
|
||||
if (start < 0 || start >= this.length || start + len < 0 || start + len > this.length) ""
|
||||
else substring(start, start + len)
|
||||
|
||||
var skipTil = 0
|
||||
for (outerI in text.indices) {
|
||||
if (skipTil > outerI) continue
|
||||
if (text[outerI] == '<') {
|
||||
for (innerI in outerI + 1 until text.length) {
|
||||
when {
|
||||
text.substringSafe(innerI, 2) == "b>" -> {
|
||||
val textStart = innerI + 2
|
||||
for (insideTagI in textStart until text.length) {
|
||||
if (text[insideTagI] == '<') {
|
||||
withStyle(bStyle) { append(text.substring(textStart, insideTagI)) }
|
||||
skipTil = insideTagI + 4
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
text.substringSafe(innerI, 2) == "i>" -> {
|
||||
val textStart = innerI + 2
|
||||
for (insideTagI in textStart until text.length) {
|
||||
if (text[insideTagI] == '<') {
|
||||
withStyle(iStyle) { append(text.substring(textStart, insideTagI)) }
|
||||
skipTil = insideTagI + 4
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
text.substringSafe(innerI, 4) == "font" -> {
|
||||
var textStart = innerI + 5
|
||||
var color = "#000000"
|
||||
for (i in textStart until text.length) {
|
||||
if (text[i] == '#') {
|
||||
color = text.substring(i, i + 7)
|
||||
textStart = i + 9
|
||||
break
|
||||
}
|
||||
}
|
||||
for (insideTagI in textStart until text.length) {
|
||||
if (text[insideTagI] == '<') {
|
||||
withStyle(fontStyle(color)) { append(text.substring(textStart, insideTagI)) }
|
||||
skipTil = insideTagI + 7
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if (skipTil > innerI) continue
|
||||
}
|
||||
} else {
|
||||
append(text[outerI])
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AnnotatedString(text)
|
||||
}
|
||||
|
||||
actual fun getAppFileUri(fileName: String): URI =
|
||||
|
||||
@@ -29,7 +29,7 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod
|
||||
m.controller.appPrefs.systemDarkTheme,
|
||||
showSettingsModal = showSettingsModal,
|
||||
editColor = { name, initialColor ->
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
ColorEditor(name, initialColor, close)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@ kotlin {
|
||||
compose {
|
||||
desktop {
|
||||
application {
|
||||
mainClass = "MainKt"
|
||||
mainClass = "chat.simplex.desktop.MainKt"
|
||||
nativeDistributions {
|
||||
modules("jdk.zipfs")
|
||||
//includeAllModules = true
|
||||
@@ -40,19 +40,19 @@ compose {
|
||||
//, TargetFormat.AppImage // Gradle doesn't sync on Mac with it
|
||||
)
|
||||
linux {
|
||||
iconFile.set(project.file("../common/src/commonMain/resources/distribute/simplex.png"))
|
||||
iconFile.set(project.file("src/jvmMain/resources/distribute/simplex.png"))
|
||||
appCategory = "Messenger"
|
||||
}
|
||||
windows {
|
||||
// LALAL
|
||||
iconFile.set(project.file("../common/src/commonMain/resources/distribute/simplex.ico"))
|
||||
iconFile.set(project.file("src/jvmMain/resources/distribute/simplex.ico"))
|
||||
console = true
|
||||
perUserInstall = true
|
||||
dirChooser = true
|
||||
}
|
||||
macOS {
|
||||
// LALAL
|
||||
//iconFile.set(project.file("../common/src/commonMain/resources/distribute/simplex.icns"))
|
||||
//iconFile.set(project.file("../desktop/src/jvmMain/resources/distribute/simplex.icns"))
|
||||
appCategory = "public.app-category.social-networking"
|
||||
bundleID = "chat.simplex.app"
|
||||
}
|
||||
@@ -116,7 +116,7 @@ afterEvaluate {
|
||||
doLast {
|
||||
copy {
|
||||
from("${project(":desktop").buildDir}/cmake/main/linux-amd64", "$cppPath/desktop/libs/linux-x86_64", "$cppPath/desktop/libs/linux-x86_64/deps")
|
||||
into("../common/src/commonMain/resources/libs/linux-x86_64")
|
||||
into("src/jvmMain/resources/libs/linux-x86_64")
|
||||
include("*.so")
|
||||
eachFile {
|
||||
path = name
|
||||
@@ -126,7 +126,7 @@ afterEvaluate {
|
||||
}
|
||||
copy {
|
||||
from("${project(":desktop").buildDir}/cmake/main/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64/deps")
|
||||
into("../common/src/commonMain/resources/libs/linux-aarch64")
|
||||
into("src/jvmMain/resources/libs/linux-aarch64")
|
||||
include("*.so")
|
||||
eachFile {
|
||||
path = name
|
||||
@@ -136,7 +136,7 @@ afterEvaluate {
|
||||
}
|
||||
copy {
|
||||
from("${project(":desktop").buildDir}/cmake/main/win-amd64", "$cppPath/desktop/libs/windows-x86_64", "$cppPath/desktop/libs/windows-x86_64/deps")
|
||||
into("../common/src/commonMain/resources/libs/windows-x86_64")
|
||||
into("src/jvmMain/resources/libs/windows-x86_64")
|
||||
include("*.dll")
|
||||
eachFile {
|
||||
path = name
|
||||
@@ -146,7 +146,7 @@ afterEvaluate {
|
||||
}
|
||||
copy {
|
||||
from("${project(":desktop").buildDir}/cmake/main/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64/deps")
|
||||
into("../common/src/commonMain/resources/libs/mac-x86_64")
|
||||
into("src/jvmMain/resources/libs/mac-x86_64")
|
||||
include("*.dylib")
|
||||
eachFile {
|
||||
path = name
|
||||
@@ -156,7 +156,7 @@ afterEvaluate {
|
||||
}
|
||||
copy {
|
||||
from("${project(":desktop").buildDir}/cmake/main/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64/deps")
|
||||
into("../common/src/commonMain/resources/libs/mac-aarch64")
|
||||
into("src/jvmMain/resources/libs/mac-aarch64")
|
||||
include("*.dylib")
|
||||
eachFile {
|
||||
path = name
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.showApp
|
||||
|
||||
fun main() {
|
||||
initHaskell()
|
||||
initApp()
|
||||
tmpDir.deleteRecursively()
|
||||
return showApp()
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package chat.simplex.desktop
|
||||
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.showApp
|
||||
import java.io.File
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
|
||||
fun main() {
|
||||
initHaskell()
|
||||
initApp()
|
||||
tmpDir.deleteRecursively()
|
||||
return showApp()
|
||||
}
|
||||
|
||||
@Suppress("UnsafeDynamicallyLoadedCode")
|
||||
private fun initHaskell() {
|
||||
val libApp = "libapp-lib.${desktopPlatform.libExtension}"
|
||||
val libsTmpDir = File(tmpDir.absolutePath + File.separator + "libs")
|
||||
copyResources(desktopPlatform.libPath, libsTmpDir.toPath())
|
||||
System.load(File(libsTmpDir, libApp).absolutePath)
|
||||
libsTmpDir.deleteRecursively()
|
||||
initHS()
|
||||
}
|
||||
|
||||
private fun copyResources(from: String, to: Path) {
|
||||
val resource = Class.forName("chat.simplex.desktop.MainKt").getResource("")!!.toURI()
|
||||
val fileSystem = FileSystems.newFileSystem(resource, emptyMap<String, String>())
|
||||
val resPath = fileSystem.getPath(from)
|
||||
Files.walkFileTree(resPath, object: SimpleFileVisitor<Path>() {
|
||||
override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||
Files.createDirectories(to.resolve(resPath.relativize(dir).toString()))
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||
Files.copy(file, to.resolve(resPath.relativize(file).toString()), StandardCopyOption.REPLACE_EXISTING)
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,6 +2,6 @@
|
||||
Type=Application
|
||||
Name=SimpleX Chat
|
||||
Comment=Private and secure open-source messenger - no user IDs (not even random numbers).
|
||||
Exec=simplex
|
||||
Icon=simplex.png
|
||||
Categories=Messenger;
|
||||
Exec=/opt/simplex/bin/simplex
|
||||
Icon=/opt/simplex/lib/simplex
|
||||
Categories=Network;Chat;
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -17,7 +17,7 @@ ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f
|
||||
cd -
|
||||
|
||||
rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||
rm -rf apps/multiplatform/common/src/commonMain/resources/libs/$OS-$ARCH/
|
||||
rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/
|
||||
rm -rf apps/multiplatform/desktop/build/cmake
|
||||
|
||||
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||
|
||||
@@ -64,7 +64,7 @@ rm deps/`basename $LIB`
|
||||
cd -
|
||||
|
||||
rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||
rm -rf apps/multiplatform/common/src/commonMain/resources/libs/$OS-$ARCH/
|
||||
rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/
|
||||
rm -rf apps/multiplatform/desktop/build/cmake
|
||||
|
||||
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||
|
||||
20
scripts/desktop/prepare-ubuntu.sh
Executable file
20
scripts/desktop/prepare-ubuntu.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo apt install git openjdk-17-jdk make cmake gcc g++ libssl-dev zlib1g zlib1g-dev pkg-config build-essential curl libffi-dev libgmp-dev libgmp10 libncurses-dev libncurses5 libtinfo5
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
|
||||
ghcup install ghc 8.10.7
|
||||
ghcup set ghc 8.10.7
|
||||
cabal update
|
||||
git clone https://github.com/simplex-chat/simplex-chat -b master simplex
|
||||
cd simplex
|
||||
echo "ignore-project: False" >> cabal.project.local
|
||||
echo "package direct-sqlcipher" >> cabal.project.local
|
||||
echo " flags: +openssl" >> cabal.project.local
|
||||
scripts/desktop/build-lib-linux.sh
|
||||
|
||||
cd apps/multiplatform
|
||||
sed -i 's|":android", ||' settings.gradle.kts
|
||||
|
||||
./gradlew packageDeb
|
||||
#sudo dpkg -i ./release/main/deb/simpl*.deb
|
||||
#sudo ln -s /opt/simplex/bin/simplex /usr/bin/simplex
|
||||
@@ -105,6 +105,7 @@ library
|
||||
Simplex.Chat.Migrations.M20230618_favorite_chats
|
||||
Simplex.Chat.Migrations.M20230621_chat_item_moderations
|
||||
Simplex.Chat.Migrations.M20230705_delivery_receipts
|
||||
Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Mobile.WebRTC
|
||||
Simplex.Chat.Options
|
||||
|
||||
@@ -159,6 +159,9 @@ maxMsgReactions = 3
|
||||
fixedImagePreview :: ImageData
|
||||
fixedImagePreview = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg=="
|
||||
|
||||
smallGroupsRcptsMemLimit :: Int
|
||||
smallGroupsRcptsMemLimit = 20
|
||||
|
||||
logCfg :: LogConfig
|
||||
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}
|
||||
|
||||
@@ -397,6 +400,12 @@ processChatCommand = \case
|
||||
withStore' $ \db -> updateUserContactReceipts db user' settings
|
||||
ok user
|
||||
SetUserContactReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserContactReceipts userId settings
|
||||
APISetUserGroupReceipts userId' settings -> withUser $ \user -> do
|
||||
user' <- privateGetUser userId'
|
||||
validateUserPassword user user' Nothing
|
||||
withStore' $ \db -> updateUserGroupReceipts db user' settings
|
||||
ok user
|
||||
SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserGroupReceipts userId settings
|
||||
APIHideUser userId' (UserPwd viewPwd) -> withUser $ \user -> do
|
||||
user' <- privateGetUser userId'
|
||||
case viewPwdHash user' of
|
||||
@@ -494,10 +503,16 @@ processChatCommand = \case
|
||||
chatItems <- withStore $ \db -> getAllChatItems db user pagination search
|
||||
pure $ CRChatItems user chatItems
|
||||
APIGetChatItemInfo chatRef itemId -> withUser $ \user -> do
|
||||
(aci@(AChatItem _ _ _ ci), versions) <- withStore $ \db ->
|
||||
(aci@(AChatItem cType dir _ ci), versions) <- withStore $ \db ->
|
||||
(,) <$> getAChatItem db user chatRef itemId <*> liftIO (getChatItemVersions db itemId)
|
||||
let itemVersions = if null versions then maybeToList $ mkItemVersion ci else versions
|
||||
pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions}
|
||||
memberDeliveryStatuses <- case (cType, dir) of
|
||||
(SCTGroup, SMDSnd) -> do
|
||||
withStore' (`getGroupSndStatuses` itemId) >>= \case
|
||||
[] -> pure Nothing
|
||||
memStatuses -> pure $ Just $ map (uncurry MemberDeliveryStatus) memStatuses
|
||||
_ -> pure Nothing
|
||||
pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses}
|
||||
APISendMessage (ChatRef cType chatId) live itemTTL (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user@User {userId} -> withChatLock "sendMessage" $ case cType of
|
||||
CTDirect -> do
|
||||
ct@Contact {contactId, localDisplayName = c, contactUsed} <- withStore $ \db -> getContact db user chatId
|
||||
@@ -572,9 +587,12 @@ processChatCommand = \case
|
||||
(fInv_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer g (length $ filter memberCurrent ms)
|
||||
timed_ <- sndGroupCITimed live gInfo itemTTL
|
||||
(msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ membership
|
||||
msg@SndMessage {sharedMsgId} <- sendGroupMessage user gInfo ms (XMsgNew msgContainer)
|
||||
(msg@SndMessage {sharedMsgId}, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer)
|
||||
mapM_ (sendGroupFileInline ms sharedMsgId) ft_
|
||||
ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live
|
||||
withStore' $ \db ->
|
||||
forM_ sentToMembers $ \GroupMember {groupMemberId} ->
|
||||
createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew
|
||||
forM_ (timed_ >>= timedDeleteAt') $
|
||||
startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci)
|
||||
setActive $ ActiveG gName
|
||||
@@ -708,7 +726,7 @@ processChatCommand = \case
|
||||
let changed = mc /= oldMC
|
||||
if changed || fromMaybe False itemLive
|
||||
then do
|
||||
SndMessage {msgId} <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive))
|
||||
(SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive))
|
||||
ci' <- withStore' $ \db -> do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
when changed $
|
||||
@@ -742,7 +760,7 @@ processChatCommand = \case
|
||||
(CIDMInternal, _, _, _) -> deleteGroupCI user gInfo ci True False Nothing =<< liftIO getCurrentTime
|
||||
(CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> do
|
||||
assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier
|
||||
SndMessage {msgId} <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId Nothing
|
||||
(SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId Nothing
|
||||
delGroupChatItem user gInfo ci msgId Nothing
|
||||
(CIDMBroadcast, _, _, _) -> throwChatError CEInvalidChatItemDelete
|
||||
CTContactRequest -> pure $ chatCmdError (Just user) "not supported"
|
||||
@@ -754,7 +772,7 @@ processChatCommand = \case
|
||||
(CIGroupRcv GroupMember {groupMemberId, memberRole, memberId}, Just itemSharedMId) -> do
|
||||
when (groupMemberId /= mId) $ throwChatError CEInvalidChatItemDelete
|
||||
assertUserGroupRole gInfo $ max GRAdmin memberRole
|
||||
SndMessage {msgId} <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId $ Just memberId
|
||||
(SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId $ Just memberId
|
||||
delGroupChatItem user gInfo ci msgId (Just membership)
|
||||
(_, _) -> throwChatError CEInvalidChatItemDelete
|
||||
APIChatItemReaction (ChatRef cType chatId) itemId add reaction -> withUser $ \user -> withChatLock "chatItemReaction" $ case cType of
|
||||
@@ -786,7 +804,7 @@ processChatCommand = \case
|
||||
let GroupMember {memberId = itemMemberId} = chatItemMember g ci
|
||||
rs <- withStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True
|
||||
checkReactionAllowed rs
|
||||
SndMessage {msgId} <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add)
|
||||
(SndMessage {msgId}, _) <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add)
|
||||
createdAt <- liftIO getCurrentTime
|
||||
reactions <- withStore' $ \db -> do
|
||||
setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt
|
||||
@@ -1409,7 +1427,7 @@ processChatCommand = \case
|
||||
(Just ct, Just cReq) -> sendGrpInvitation user ct gInfo (m :: GroupMember) {memberRole = memRole} cReq
|
||||
_ -> throwChatError $ CEGroupCantResendInvitation gInfo cName
|
||||
_ -> do
|
||||
msg <- sendGroupMessage user gInfo members $ XGrpMemRole mId memRole
|
||||
(msg, _) <- sendGroupMessage user gInfo members $ XGrpMemRole mId memRole
|
||||
ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent gEvent)
|
||||
toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci)
|
||||
pure CRMemberRoleUser {user, groupInfo = gInfo, member = m {memberRole = memRole}, fromRole = mRole, toRole = memRole}
|
||||
@@ -1425,7 +1443,7 @@ processChatCommand = \case
|
||||
deleteMemberConnection user m
|
||||
withStore' $ \db -> deleteGroupMember db user m
|
||||
_ -> do
|
||||
msg <- sendGroupMessage user gInfo members $ XGrpMemDel mId
|
||||
(msg, _) <- sendGroupMessage user gInfo members $ XGrpMemDel mId
|
||||
ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent $ SGEMemberDeleted memberId (fromLocalProfile memberProfile))
|
||||
toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci)
|
||||
deleteMemberConnection user m
|
||||
@@ -1435,7 +1453,7 @@ processChatCommand = \case
|
||||
APILeaveGroup groupId -> withUser $ \user@User {userId} -> do
|
||||
Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db user groupId
|
||||
withChatLock "leaveGroup" . procCmd $ do
|
||||
msg <- sendGroupMessage user gInfo members XGrpLeave
|
||||
(msg, _) <- sendGroupMessage user gInfo members XGrpLeave
|
||||
ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft)
|
||||
toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci)
|
||||
-- TODO delete direct connections that were unused
|
||||
@@ -1823,7 +1841,7 @@ processChatCommand = \case
|
||||
runUpdateGroupProfile user (Group g@GroupInfo {groupProfile = p} ms) p' = do
|
||||
assertUserGroupRole g GROwner
|
||||
g' <- withStore $ \db -> updateGroupProfile db user g p'
|
||||
msg <- sendGroupMessage user g' ms (XGrpInfo p')
|
||||
(msg, _) <- sendGroupMessage user g' ms (XGrpInfo p')
|
||||
let cd = CDGroupSnd g'
|
||||
unless (sameGroupProfileInfo p p') $ do
|
||||
ci <- saveSndChatItem user cd msg (CISndGroupEvent $ SGEGroupUpdated p')
|
||||
@@ -2871,12 +2889,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
SENT msgId -> do
|
||||
sentMsgDeliveryEvent conn msgId
|
||||
checkSndInlineFTComplete conn msgId
|
||||
withStore' (\db -> getDirectChatItemByAgentMsgId db user contactId connId msgId) >>= \case
|
||||
Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _}}) -> pure ()
|
||||
Just (CChatItem SMDSnd ci) -> do
|
||||
chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId (chatItemId' ci) CISSndSent
|
||||
toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem)
|
||||
_ -> pure ()
|
||||
updateDirectItemStatus ct conn msgId $ CISSndSent SSPComplete
|
||||
SWITCH qd phase cStats -> do
|
||||
toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats)
|
||||
when (phase `elem` [SPStarted, SPCompleted]) $ case qd of
|
||||
@@ -2917,10 +2930,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} ->
|
||||
when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId
|
||||
MERR msgId err -> do
|
||||
chatItemId_ <- withStore' $ \db -> getChatItemIdByAgentMsgId db connId msgId
|
||||
forM_ chatItemId_ $ \chatItemId -> do
|
||||
chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId chatItemId (agentErrToItemStatus err)
|
||||
toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem)
|
||||
updateDirectItemStatus ct conn msgId $ agentErrToItemStatus err
|
||||
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
incAuthErrCounter connEntity conn err
|
||||
ERR err -> do
|
||||
@@ -3066,7 +3076,12 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
XGrpInfo p' -> xGrpInfo gInfo m p' msg msgMeta
|
||||
BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta
|
||||
_ -> messageError $ "unsupported message: " <> T.pack (show event)
|
||||
pure False -- no receipts in group now $ hasDeliveryReceipt $ toCMEventTag event
|
||||
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
|
||||
let GroupInfo {chatSettings = ChatSettings {sendRcpts}} = gInfo
|
||||
pure $
|
||||
fromMaybe (sendRcptsSmallGroups user) sendRcpts
|
||||
&& hasDeliveryReceipt (toCMEventTag event)
|
||||
&& currentMemCount <= smallGroupsRcptsMemLimit
|
||||
where
|
||||
canSend a
|
||||
| memberRole (m :: GroupMember) <= GRObserver = messageError "member is not allowed to send messages"
|
||||
@@ -3077,6 +3092,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
SENT msgId -> do
|
||||
sentMsgDeliveryEvent conn msgId
|
||||
checkSndInlineFTComplete conn msgId
|
||||
updateGroupItemStatus gInfo m conn msgId $ CISSndSent SSPComplete
|
||||
SWITCH qd phase cStats -> do
|
||||
toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats)
|
||||
when (phase `elem` [SPStarted, SPCompleted]) $ case qd of
|
||||
@@ -3113,7 +3129,11 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
-- [async agent commands] continuation on receiving OK
|
||||
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} ->
|
||||
when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId
|
||||
MERR _ err -> do
|
||||
MERR msgId err -> do
|
||||
chatItemId_ <- withStore' $ \db -> getChatItemIdByAgentMsgId db connId msgId
|
||||
forM_ chatItemId_ $ \itemId -> do
|
||||
let GroupMember {groupMemberId} = m
|
||||
updateGroupMemSndStatus itemId groupMemberId $ agentErrToItemStatus err
|
||||
-- group errors are silenced to reduce load on UI event log
|
||||
-- toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
|
||||
incAuthErrCounter connEntity conn err
|
||||
@@ -3368,7 +3388,6 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
-- 1) retry processing several times
|
||||
-- 2) stabilize database
|
||||
-- 3) show screen of death to the user asking to restart
|
||||
-- TODO send receipt depending on contact/group settings
|
||||
tryChatError action >>= \case
|
||||
Right withRcpt -> ack $ if withRcpt then Just "" else Nothing
|
||||
Left e -> ack Nothing >> throwError e
|
||||
@@ -4295,21 +4314,52 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
createGroupFeatureChangedItems user cd CIRcvGroupFeature g g'
|
||||
|
||||
directMsgReceived :: Contact -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> m ()
|
||||
directMsgReceived ct@Contact {contactId} Connection {connId} msgMeta msgRcpts = do
|
||||
directMsgReceived ct conn@Connection {connId} msgMeta msgRcpts = do
|
||||
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
|
||||
forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do
|
||||
withStore $ \db -> createSndMsgDeliveryEvent db connId agentMsgId $ MDSSndRcvd msgRcptStatus
|
||||
withStore' (\db -> getDirectChatItemByAgentMsgId db user contactId connId agentMsgId) >>= \case
|
||||
Just (CChatItem SMDSnd ci) -> do
|
||||
chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId (chatItemId' ci) $ CISSndRcvd msgRcptStatus
|
||||
toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem)
|
||||
_ -> pure ()
|
||||
updateDirectItemStatus ct conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete
|
||||
|
||||
groupMsgReceived :: GroupInfo -> GroupMember -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> m ()
|
||||
groupMsgReceived gInfo m Connection {connId} msgMeta msgRcpts = do
|
||||
groupMsgReceived gInfo m conn@Connection {connId} msgMeta msgRcpts = do
|
||||
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta
|
||||
forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} ->
|
||||
forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do
|
||||
withStore $ \db -> createSndMsgDeliveryEvent db connId agentMsgId $ MDSSndRcvd msgRcptStatus
|
||||
updateGroupItemStatus gInfo m conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete
|
||||
|
||||
updateDirectItemStatus :: Contact -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> m ()
|
||||
updateDirectItemStatus ct@Contact {contactId} Connection {connId} msgId newStatus =
|
||||
withStore' (\db -> getDirectChatItemByAgentMsgId db user contactId connId msgId) >>= \case
|
||||
Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ _}}) -> pure ()
|
||||
Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}})
|
||||
| itemStatus == newStatus -> pure ()
|
||||
| otherwise -> do
|
||||
chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId itemId newStatus
|
||||
toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem)
|
||||
_ -> pure ()
|
||||
|
||||
updateGroupMemSndStatus :: ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> m Bool
|
||||
updateGroupMemSndStatus itemId groupMemberId newStatus =
|
||||
runExceptT (withStore $ \db -> getGroupSndStatus db itemId groupMemberId) >>= \case
|
||||
Right (CISSndRcvd _ _) -> pure False
|
||||
Right memStatus
|
||||
| memStatus == newStatus -> pure False
|
||||
| otherwise -> withStore' (\db -> updateGroupSndStatus db itemId groupMemberId newStatus) $> True
|
||||
_ -> pure False
|
||||
|
||||
updateGroupItemStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> m ()
|
||||
updateGroupItemStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus =
|
||||
withStore' (\db -> getGroupChatItemByAgentMsgId db user groupId connId msgId) >>= \case
|
||||
Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ SSPComplete}}) -> pure ()
|
||||
Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}}) -> do
|
||||
memStatusChanged <- updateGroupMemSndStatus itemId groupMemberId newMemStatus
|
||||
when memStatusChanged $ do
|
||||
memStatusCounts <- withStore' (`getGroupSndStatusCounts` itemId)
|
||||
let newStatus = membersGroupItemStatus memStatusCounts
|
||||
when (newStatus /= itemStatus) $ do
|
||||
chatItem <- withStore $ \db -> updateGroupChatItemStatus db user groupId itemId newStatus
|
||||
toView $ CRChatItemStatusUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) chatItem)
|
||||
_ -> pure ()
|
||||
|
||||
parseFileDescription :: (ChatMonad m, FilePartyI p) => Text -> m (ValidFileDescription p)
|
||||
parseFileDescription =
|
||||
@@ -4525,26 +4575,33 @@ deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do
|
||||
(Just $ "createSndMsgDelivery, sndMsgDelivery: " <> show sndMsgDelivery <> ", msgId: " <> show msgId <> ", cmEventTag: " <> show cmEventTag <> ", msgDeliveryStatus: MDSSndAgent")
|
||||
$ \db -> createSndMsgDelivery db sndMsgDelivery msgId
|
||||
|
||||
sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m SndMessage
|
||||
sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m (SndMessage, [GroupMember])
|
||||
sendGroupMessage user GroupInfo {groupId} members chatMsgEvent =
|
||||
sendGroupMessage' user members chatMsgEvent groupId Nothing $ pure ()
|
||||
|
||||
sendGroupMessage' :: (MsgEncodingI e, ChatMonad m) => User -> [GroupMember] -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m SndMessage
|
||||
sendGroupMessage' :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> [GroupMember] -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m (SndMessage, [GroupMember])
|
||||
sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do
|
||||
msg <- createSndMessage chatMsgEvent (GroupId groupId)
|
||||
-- TODO collect failed deliveries into a single error
|
||||
forM_ (filter memberCurrent members) $ \m ->
|
||||
messageMember m msg `catchChatError` (toView . CRChatError (Just user))
|
||||
pure msg
|
||||
rs <- forM (filter memberCurrent members) $ \m ->
|
||||
messageMember m msg `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing)
|
||||
let sentToMembers = catMaybes rs
|
||||
pure (msg, sentToMembers)
|
||||
where
|
||||
messageMember :: GroupMember -> SndMessage -> m (Maybe GroupMember)
|
||||
messageMember m@GroupMember {groupMemberId} SndMessage {msgId, msgBody} = case memberConn m of
|
||||
Nothing -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
|
||||
Nothing -> do
|
||||
withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
|
||||
pure $ Just m
|
||||
Just conn@Connection {connStatus}
|
||||
| connDisabled conn || connStatus == ConnDeleted -> pure ()
|
||||
| connDisabled conn || connStatus == ConnDeleted -> pure Nothing
|
||||
| connStatus == ConnSndReady || connStatus == ConnReady -> do
|
||||
let tag = toCMEventTag chatMsgEvent
|
||||
deliverMessage conn tag msgBody msgId >> postDeliver
|
||||
| otherwise -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
|
||||
pure $ Just m
|
||||
| otherwise -> do
|
||||
withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
|
||||
pure $ Just m
|
||||
|
||||
sendPendingGroupMessages :: ChatMonad m => User -> GroupMember -> Connection -> m ()
|
||||
sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn = do
|
||||
@@ -4926,8 +4983,10 @@ chatCommandP =
|
||||
"/_user " *> (APISetActiveUser <$> A.decimal <*> optional (A.space *> jsonP)),
|
||||
("/user " <|> "/u ") *> (SetActiveUser <$> displayName <*> optional (A.space *> pwdP)),
|
||||
"/set receipts all " *> (SetAllContactReceipts <$> onOffP),
|
||||
"/_set receipts " *> (APISetUserContactReceipts <$> A.decimal <* A.space <*> receiptSettings),
|
||||
"/set receipts " *> (SetUserContactReceipts <$> receiptSettings),
|
||||
"/_set receipts contacts " *> (APISetUserContactReceipts <$> A.decimal <* A.space <*> receiptSettings),
|
||||
"/set receipts contacts " *> (SetUserContactReceipts <$> receiptSettings),
|
||||
"/_set receipts groups " *> (APISetUserGroupReceipts <$> A.decimal <* A.space <*> receiptSettings),
|
||||
"/set receipts groups " *> (SetUserGroupReceipts <$> receiptSettings),
|
||||
"/_hide user " *> (APIHideUser <$> A.decimal <* A.space <*> jsonP),
|
||||
"/_unhide user " *> (APIUnhideUser <$> A.decimal <* A.space <*> jsonP),
|
||||
"/_mute user " *> (APIMuteUser <$> A.decimal),
|
||||
|
||||
@@ -203,6 +203,8 @@ data ChatCommand
|
||||
| SetAllContactReceipts Bool
|
||||
| APISetUserContactReceipts UserId UserMsgReceiptSettings
|
||||
| SetUserContactReceipts UserMsgReceiptSettings
|
||||
| APISetUserGroupReceipts UserId UserMsgReceiptSettings
|
||||
| SetUserGroupReceipts UserMsgReceiptSettings
|
||||
| APIHideUser UserId UserPwd
|
||||
| APIUnhideUser UserId UserPwd
|
||||
| APIMuteUser UserId
|
||||
|
||||
@@ -21,7 +21,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.ByteString.Lazy.Char8 as LB
|
||||
import Data.Int (Int64)
|
||||
import Data.Maybe (isJust, isNothing)
|
||||
import Data.Maybe (fromMaybe, isJust, isNothing)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
@@ -624,13 +624,15 @@ data CIFileInfo = CIFileInfo
|
||||
|
||||
data CIStatus (d :: MsgDirection) where
|
||||
CISSndNew :: CIStatus 'MDSnd
|
||||
CISSndSent :: CIStatus 'MDSnd
|
||||
CISSndRcvd :: MsgReceiptStatus -> CIStatus 'MDSnd
|
||||
CISSndSent :: SndCIStatusProgress -> CIStatus 'MDSnd
|
||||
CISSndRcvd :: MsgReceiptStatus -> SndCIStatusProgress -> CIStatus 'MDSnd
|
||||
CISSndErrorAuth :: CIStatus 'MDSnd
|
||||
CISSndError :: String -> CIStatus 'MDSnd
|
||||
CISRcvNew :: CIStatus 'MDRcv
|
||||
CISRcvRead :: CIStatus 'MDRcv
|
||||
|
||||
deriving instance Eq (CIStatus d)
|
||||
|
||||
deriving instance Show (CIStatus d)
|
||||
|
||||
instance ToJSON (CIStatus d) where
|
||||
@@ -639,6 +641,8 @@ instance ToJSON (CIStatus d) where
|
||||
|
||||
instance MsgDirectionI d => ToField (CIStatus d) where toField = toField . decodeLatin1 . strEncode
|
||||
|
||||
instance (Typeable d, MsgDirectionI d) => FromField (CIStatus d) where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
|
||||
|
||||
instance FromField ACIStatus where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
|
||||
|
||||
data ACIStatus = forall d. MsgDirectionI d => ACIStatus (SMsgDirection d) (CIStatus d)
|
||||
@@ -648,8 +652,8 @@ deriving instance Show ACIStatus
|
||||
instance MsgDirectionI d => StrEncoding (CIStatus d) where
|
||||
strEncode = \case
|
||||
CISSndNew -> "snd_new"
|
||||
CISSndSent -> "snd_sent"
|
||||
CISSndRcvd status -> "snd_rcvd " <> strEncode status
|
||||
CISSndSent sndProgress -> "snd_sent " <> strEncode sndProgress
|
||||
CISSndRcvd msgRcptStatus sndProgress -> "snd_rcvd " <> strEncode msgRcptStatus <> " " <> strEncode sndProgress
|
||||
CISSndErrorAuth -> "snd_error_auth"
|
||||
CISSndError e -> "snd_error " <> encodeUtf8 (T.pack e)
|
||||
CISRcvNew -> "rcv_new"
|
||||
@@ -661,8 +665,8 @@ instance StrEncoding ACIStatus where
|
||||
strP =
|
||||
A.takeTill (== ' ') >>= \case
|
||||
"snd_new" -> pure $ ACIStatus SMDSnd CISSndNew
|
||||
"snd_sent" -> pure $ ACIStatus SMDSnd CISSndSent
|
||||
"snd_rcvd" -> ACIStatus SMDSnd . CISSndRcvd <$> (A.space *> strP)
|
||||
"snd_sent" -> ACIStatus SMDSnd . CISSndSent <$> ((A.space *> strP) <|> pure SSPComplete)
|
||||
"snd_rcvd" -> ACIStatus SMDSnd <$> (CISSndRcvd <$> (A.space *> strP) <*> ((A.space *> strP) <|> pure SSPComplete))
|
||||
"snd_error_auth" -> pure $ ACIStatus SMDSnd CISSndErrorAuth
|
||||
"snd_error" -> ACIStatus SMDSnd . CISSndError . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
|
||||
"rcv_new" -> pure $ ACIStatus SMDRcv CISRcvNew
|
||||
@@ -671,8 +675,8 @@ instance StrEncoding ACIStatus where
|
||||
|
||||
data JSONCIStatus
|
||||
= JCISSndNew
|
||||
| JCISSndSent
|
||||
| JCISSndRcvd {msgRcptStatus :: MsgReceiptStatus}
|
||||
| JCISSndSent {sndProgress :: SndCIStatusProgress}
|
||||
| JCISSndRcvd {msgRcptStatus :: MsgReceiptStatus, sndProgress :: SndCIStatusProgress}
|
||||
| JCISSndErrorAuth
|
||||
| JCISSndError {agentError :: String}
|
||||
| JCISRcvNew
|
||||
@@ -686,8 +690,8 @@ instance ToJSON JSONCIStatus where
|
||||
jsonCIStatus :: CIStatus d -> JSONCIStatus
|
||||
jsonCIStatus = \case
|
||||
CISSndNew -> JCISSndNew
|
||||
CISSndSent -> JCISSndSent
|
||||
CISSndRcvd ok -> JCISSndRcvd ok
|
||||
CISSndSent sndProgress -> JCISSndSent sndProgress
|
||||
CISSndRcvd msgRcptStatus sndProgress -> JCISSndRcvd msgRcptStatus sndProgress
|
||||
CISSndErrorAuth -> JCISSndErrorAuth
|
||||
CISSndError e -> JCISSndError e
|
||||
CISRcvNew -> JCISRcvNew
|
||||
@@ -703,6 +707,40 @@ ciCreateStatus content = case msgDirection @d of
|
||||
SMDSnd -> ciStatusNew
|
||||
SMDRcv -> if ciRequiresAttention content then ciStatusNew else CISRcvRead
|
||||
|
||||
membersGroupItemStatus :: [(CIStatus 'MDSnd, Int)] -> CIStatus 'MDSnd
|
||||
membersGroupItemStatus memStatusCounts
|
||||
| rcvdOk == total = CISSndRcvd MROk SSPComplete
|
||||
| rcvdOk + rcvdBad == total = CISSndRcvd MRBadMsgHash SSPComplete
|
||||
| rcvdBad > 0 = CISSndRcvd MRBadMsgHash SSPPartial
|
||||
| rcvdOk > 0 = CISSndRcvd MROk SSPPartial
|
||||
| sent == total = CISSndSent SSPComplete
|
||||
| sent > 0 = CISSndSent SSPPartial
|
||||
| otherwise = CISSndNew
|
||||
where
|
||||
total = sum $ map snd memStatusCounts
|
||||
rcvdOk = fromMaybe 0 $ lookup (CISSndRcvd MROk SSPComplete) memStatusCounts
|
||||
rcvdBad = fromMaybe 0 $ lookup (CISSndRcvd MRBadMsgHash SSPComplete) memStatusCounts
|
||||
sent = fromMaybe 0 $ lookup (CISSndSent SSPComplete) memStatusCounts
|
||||
|
||||
data SndCIStatusProgress
|
||||
= SSPPartial
|
||||
| SSPComplete
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON SndCIStatusProgress where
|
||||
toJSON = J.genericToJSON . enumJSON $ dropPrefix "SSP"
|
||||
toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "SSP"
|
||||
|
||||
instance StrEncoding SndCIStatusProgress where
|
||||
strEncode = \case
|
||||
SSPPartial -> "partial"
|
||||
SSPComplete -> "complete"
|
||||
strP =
|
||||
A.takeWhile1 (/= ' ') >>= \case
|
||||
"partial" -> pure SSPPartial
|
||||
"complete" -> pure SSPComplete
|
||||
_ -> fail "bad SndCIStatusProgress"
|
||||
|
||||
type ChatItemId = Int64
|
||||
|
||||
type ChatItemTs = UTCTime
|
||||
@@ -887,7 +925,8 @@ itemDeletedTs = \case
|
||||
CIModerated ts _ -> ts
|
||||
|
||||
data ChatItemInfo = ChatItemInfo
|
||||
{ itemVersions :: [ChatItemVersion]
|
||||
{ itemVersions :: [ChatItemVersion],
|
||||
memberDeliveryStatuses :: Maybe [MemberDeliveryStatus]
|
||||
}
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
@@ -917,6 +956,14 @@ mkItemVersion ChatItem {content, meta} = version <$> ciMsgContent content
|
||||
createdAt = createdAt
|
||||
}
|
||||
|
||||
data MemberDeliveryStatus = MemberDeliveryStatus
|
||||
{ groupMemberId :: GroupMemberId,
|
||||
memberDeliveryStatus :: CIStatus 'MDSnd
|
||||
}
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON MemberDeliveryStatus where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
data CIModeration = CIModeration
|
||||
{ moderationId :: Int64,
|
||||
moderatorMember :: GroupMember,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20230721_group_snd_item_statuses where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20230721_group_snd_item_statuses :: Query
|
||||
m20230721_group_snd_item_statuses =
|
||||
[sql|
|
||||
CREATE TABLE group_snd_item_statuses (
|
||||
group_snd_item_status_id INTEGER PRIMARY KEY,
|
||||
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
|
||||
group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE,
|
||||
group_snd_item_status TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_group_snd_item_statuses_chat_item_id ON group_snd_item_statuses(chat_item_id);
|
||||
CREATE INDEX idx_group_snd_item_statuses_group_member_id ON group_snd_item_statuses(group_member_id);
|
||||
|
||||
UPDATE users SET send_rcpts_small_groups = 1 WHERE send_rcpts_contacts = 1;
|
||||
|]
|
||||
|
||||
down_m20230721_group_snd_item_statuses :: Query
|
||||
down_m20230721_group_snd_item_statuses =
|
||||
[sql|
|
||||
DROP INDEX idx_group_snd_item_statuses_group_member_id;
|
||||
DROP INDEX idx_group_snd_item_statuses_chat_item_id;
|
||||
|
||||
DROP TABLE group_snd_item_statuses;
|
||||
|]
|
||||
@@ -496,6 +496,14 @@ CREATE TABLE chat_item_moderations(
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE TABLE group_snd_item_statuses(
|
||||
group_snd_item_status_id INTEGER PRIMARY KEY,
|
||||
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
|
||||
group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE,
|
||||
group_snd_item_status TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
);
|
||||
CREATE INDEX contact_profiles_index ON contact_profiles(
|
||||
display_name,
|
||||
full_name
|
||||
@@ -687,3 +695,9 @@ CREATE INDEX idx_chat_item_moderations_group ON chat_item_moderations(
|
||||
item_member_id,
|
||||
shared_msg_id
|
||||
);
|
||||
CREATE INDEX idx_group_snd_item_statuses_chat_item_id ON group_snd_item_statuses(
|
||||
chat_item_id
|
||||
);
|
||||
CREATE INDEX idx_group_snd_item_statuses_group_member_id ON group_snd_item_statuses(
|
||||
group_member_id
|
||||
);
|
||||
|
||||
@@ -39,6 +39,7 @@ module Simplex.Chat.Store.Groups
|
||||
getGroupMemberById,
|
||||
getGroupMembers,
|
||||
getGroupMembersForExpiration,
|
||||
getGroupCurrentMembersCount,
|
||||
deleteGroupConnectionsAndFiles,
|
||||
deleteGroupItemsAndMembers,
|
||||
deleteGroup,
|
||||
@@ -548,6 +549,20 @@ toContactMember :: User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember
|
||||
toContactMember User {userContactId} (memberRow :. connRow) =
|
||||
(toGroupMember userContactId memberRow) {activeConn = toMaybeConnection connRow}
|
||||
|
||||
getGroupCurrentMembersCount :: DB.Connection -> User -> GroupInfo -> IO Int
|
||||
getGroupCurrentMembersCount db User {userId} GroupInfo {groupId} = do
|
||||
statuses :: [GroupMemberStatus] <-
|
||||
map fromOnly
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT member_status
|
||||
FROM group_members
|
||||
WHERE group_id = ? AND user_id = ?
|
||||
|]
|
||||
(groupId, userId)
|
||||
pure $ length $ filter memberCurrent' statuses
|
||||
|
||||
getGroupInvitation :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation
|
||||
getGroupInvitation db user groupId =
|
||||
getConnRec_ user >>= \case
|
||||
|
||||
@@ -44,6 +44,7 @@ module Simplex.Chat.Store.Messages
|
||||
createChatItemVersion,
|
||||
deleteDirectChatItem,
|
||||
markDirectChatItemDeleted,
|
||||
updateGroupChatItemStatus,
|
||||
updateGroupChatItem,
|
||||
deleteGroupChatItem,
|
||||
updateGroupChatItemModerated,
|
||||
@@ -69,6 +70,7 @@ module Simplex.Chat.Store.Messages
|
||||
getGroupChatItem,
|
||||
getGroupChatItemBySharedMsgId,
|
||||
getGroupMemberCIBySharedMsgId,
|
||||
getGroupChatItemByAgentMsgId,
|
||||
getGroupMemberChatItemLast,
|
||||
getDirectChatItemIdByText,
|
||||
getDirectChatItemIdByText',
|
||||
@@ -87,6 +89,11 @@ module Simplex.Chat.Store.Messages
|
||||
createCIModeration,
|
||||
getCIModeration,
|
||||
deleteCIModeration,
|
||||
createGroupSndStatus,
|
||||
getGroupSndStatus,
|
||||
updateGroupSndStatus,
|
||||
getGroupSndStatuses,
|
||||
getGroupSndStatusCounts,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -1325,6 +1332,16 @@ getDirectChatItemIdByText' db User {userId} contactId msg =
|
||||
|]
|
||||
(userId, contactId, msg <> "%")
|
||||
|
||||
updateGroupChatItemStatus :: forall d. MsgDirectionI d => DB.Connection -> User -> GroupId -> ChatItemId -> CIStatus d -> ExceptT StoreError IO (ChatItem 'CTGroup d)
|
||||
updateGroupChatItemStatus db user@User {userId} groupId itemId itemStatus = do
|
||||
ci <- liftEither . correctDir =<< getGroupChatItem db user groupId itemId
|
||||
currentTs <- liftIO getCurrentTime
|
||||
liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND group_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, groupId, itemId)
|
||||
pure ci {meta = (meta ci) {itemStatus}}
|
||||
where
|
||||
correctDir :: CChatItem c -> Either StoreError (ChatItem c d)
|
||||
correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci
|
||||
|
||||
updateGroupChatItem :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTGroup d)
|
||||
updateGroupChatItem db user groupId ci newContent live msgId_ = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
@@ -1434,6 +1451,11 @@ getGroupMemberCIBySharedMsgId db user@User {userId} groupId memberId sharedMsgId
|
||||
(GCUserMember, userId, groupId, memberId, sharedMsgId)
|
||||
getGroupChatItem db user groupId itemId
|
||||
|
||||
getGroupChatItemByAgentMsgId :: DB.Connection -> User -> GroupId -> Int64 -> AgentMsgId -> IO (Maybe (CChatItem 'CTGroup))
|
||||
getGroupChatItemByAgentMsgId db user groupId connId msgId = do
|
||||
itemId_ <- getChatItemIdByAgentMsgId db connId msgId
|
||||
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupChatItem db user groupId) itemId_
|
||||
|
||||
getGroupChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTGroup)
|
||||
getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
|
||||
currentTs <- getCurrentTime
|
||||
@@ -1847,3 +1869,58 @@ deleteCIModeration db GroupInfo {groupId} itemMemberId (Just sharedMsgId) =
|
||||
db
|
||||
"DELETE FROM chat_item_moderations WHERE group_id = ? AND item_member_id = ? AND shared_msg_id = ?"
|
||||
(groupId, itemMemberId, sharedMsgId)
|
||||
|
||||
createGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> IO ()
|
||||
createGroupSndStatus db itemId memberId status =
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO group_snd_item_statuses (chat_item_id, group_member_id, group_snd_item_status) VALUES (?,?,?)"
|
||||
(itemId, memberId, status)
|
||||
|
||||
getGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> ExceptT StoreError IO (CIStatus 'MDSnd)
|
||||
getGroupSndStatus db itemId memberId =
|
||||
ExceptT . firstRow fromOnly (SENoGroupSndStatus itemId memberId) $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT group_snd_item_status
|
||||
FROM group_snd_item_statuses
|
||||
WHERE chat_item_id = ? AND group_member_id = ?
|
||||
LIMIT 1
|
||||
|]
|
||||
(itemId, memberId)
|
||||
|
||||
updateGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> IO ()
|
||||
updateGroupSndStatus db itemId memberId status = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_snd_item_statuses
|
||||
SET group_snd_item_status = ?, updated_at = ?
|
||||
WHERE chat_item_id = ? AND group_member_id = ?
|
||||
|]
|
||||
(status, currentTs, itemId, memberId)
|
||||
|
||||
getGroupSndStatuses :: DB.Connection -> ChatItemId -> IO [(GroupMemberId, CIStatus 'MDSnd)]
|
||||
getGroupSndStatuses db itemId =
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT group_member_id, group_snd_item_status
|
||||
FROM group_snd_item_statuses
|
||||
WHERE chat_item_id = ?
|
||||
|]
|
||||
(Only itemId)
|
||||
|
||||
getGroupSndStatusCounts :: DB.Connection -> ChatItemId -> IO [(CIStatus 'MDSnd, Int)]
|
||||
getGroupSndStatusCounts db itemId =
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT group_snd_item_status, COUNT(1)
|
||||
FROM group_snd_item_statuses
|
||||
WHERE chat_item_id = ?
|
||||
GROUP BY group_snd_item_status
|
||||
|]
|
||||
(Only itemId)
|
||||
|
||||
@@ -74,6 +74,7 @@ import Simplex.Chat.Migrations.M20230608_deleted_contacts
|
||||
import Simplex.Chat.Migrations.M20230618_favorite_chats
|
||||
import Simplex.Chat.Migrations.M20230621_chat_item_moderations
|
||||
import Simplex.Chat.Migrations.M20230705_delivery_receipts
|
||||
import Simplex.Chat.Migrations.M20230721_group_snd_item_statuses
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -147,7 +148,8 @@ schemaMigrations =
|
||||
("20230608_deleted_contacts", m20230608_deleted_contacts, Just down_m20230608_deleted_contacts),
|
||||
("20230618_favorite_chats", m20230618_favorite_chats, Just down_m20230618_favorite_chats),
|
||||
("20230621_chat_item_moderations", m20230621_chat_item_moderations, Just down_m20230621_chat_item_moderations),
|
||||
("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts)
|
||||
("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts),
|
||||
("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -30,6 +30,7 @@ module Simplex.Chat.Store.Profiles
|
||||
updateUserPrivacy,
|
||||
updateAllContactReceipts,
|
||||
updateUserContactReceipts,
|
||||
updateUserGroupReceipts,
|
||||
updateUserProfile,
|
||||
setUserProfileContactLink,
|
||||
getUserContactProfiles,
|
||||
@@ -92,7 +93,7 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, image,
|
||||
when activeUser $ DB.execute_ db "UPDATE users SET active_user = 0"
|
||||
let showNtfs = True
|
||||
sendRcptsContacts = True
|
||||
sendRcptsSmallGroups = False
|
||||
sendRcptsSmallGroups = True
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO users (agent_user_id, local_display_name, active_user, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, created_at, updated_at) VALUES (?,?,?,0,?,?,?,?,?)"
|
||||
@@ -222,13 +223,21 @@ updateUserPrivacy db User {userId, showNtfs, viewPwdHash} =
|
||||
|
||||
updateAllContactReceipts :: DB.Connection -> Bool -> IO ()
|
||||
updateAllContactReceipts db onOff =
|
||||
DB.execute db "UPDATE users SET send_rcpts_contacts = ? WHERE view_pwd_hash IS NULL" (Only onOff)
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE users SET send_rcpts_contacts = ?, send_rcpts_small_groups = ? WHERE view_pwd_hash IS NULL"
|
||||
(onOff, onOff)
|
||||
|
||||
updateUserContactReceipts :: DB.Connection -> User -> UserMsgReceiptSettings -> IO ()
|
||||
updateUserContactReceipts db User {userId} UserMsgReceiptSettings {enable, clearOverrides} = do
|
||||
DB.execute db "UPDATE users SET send_rcpts_contacts = ? WHERE user_id = ?" (enable, userId)
|
||||
when clearOverrides $ DB.execute_ db "UPDATE contacts SET send_rcpts = NULL"
|
||||
|
||||
updateUserGroupReceipts :: DB.Connection -> User -> UserMsgReceiptSettings -> IO ()
|
||||
updateUserGroupReceipts db User {userId} UserMsgReceiptSettings {enable, clearOverrides} = do
|
||||
DB.execute db "UPDATE users SET send_rcpts_small_groups = ? WHERE user_id = ?" (enable, userId)
|
||||
when clearOverrides $ DB.execute_ db "UPDATE groups SET send_rcpts = NULL"
|
||||
|
||||
updateUserProfile :: DB.Connection -> User -> Profile -> ExceptT StoreError IO User
|
||||
updateUserProfile db user p'
|
||||
| displayName == newName = do
|
||||
|
||||
@@ -92,6 +92,7 @@ data StoreError
|
||||
| SEGroupLinkNotFound {groupInfo :: GroupInfo}
|
||||
| SEHostMemberIdNotFound {groupId :: Int64}
|
||||
| SEContactNotFoundByFileId {fileId :: FileTransferId}
|
||||
| SENoGroupSndStatus {itemId :: ChatItemId, groupMemberId :: GroupMemberId}
|
||||
deriving (Show, Exception, Generic)
|
||||
|
||||
instance ToJSON StoreError where
|
||||
|
||||
@@ -782,7 +782,10 @@ memberActive m = case memberStatus m of
|
||||
GSMemCreator -> True
|
||||
|
||||
memberCurrent :: GroupMember -> Bool
|
||||
memberCurrent m = case memberStatus m of
|
||||
memberCurrent = memberCurrent' . memberStatus
|
||||
|
||||
memberCurrent' :: GroupMemberStatus -> Bool
|
||||
memberCurrent' = \case
|
||||
GSMemRemoved -> False
|
||||
GSMemLeft -> False
|
||||
GSMemGroupDeleted -> False
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user