Merge branch 'master' into master-ios

This commit is contained in:
spaced4ndy
2023-07-28 11:10:08 +04:00
55 changed files with 370 additions and 177 deletions
@@ -95,6 +95,7 @@ kotlin {
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") {
@@ -47,7 +47,8 @@ actual fun PlatformTextField(
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
onMessageChange: (String) -> Unit
onMessageChange: (String) -> Unit,
onDone: () -> Unit,
) {
val cs = composeState.value
val textColor = MaterialTheme.colors.onBackground
@@ -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(
@@ -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;
@@ -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,
@@ -56,7 +56,7 @@ object ChatModel {
val chatItems = mutableStateListOf<ChatItem>()
val groupMembers = mutableStateListOf<GroupMember>()
val terminalItems = mutableStateListOf<TerminalItem>()
val terminalItems = mutableStateOf(emptyList<TerminalItem>())
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
// Allows to temporary save servers that are being edited on multiple screens
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
@@ -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) {
@@ -483,10 +484,11 @@ object ChatModel {
networkStatuses[contact.activeConn.agentConnId] ?: NetworkStatus.Unknown()
fun addTerminalItem(item: TerminalItem) {
if (terminalItems.size >= 500) {
terminalItems.removeAt(0)
if (terminalItems.value.size >= 500) {
terminalItems.value = terminalItems.value.takeLast(499) + item
} else {
terminalItems.value += item
}
terminalItems.add(item)
}
}
@@ -22,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()
@@ -11,5 +11,6 @@ expect fun PlatformTextField(
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
onMessageChange: (String) -> Unit
onMessageChange: (String) -> Unit,
onDone: () -> Unit,
)
@@ -34,7 +34,7 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
close()
})
TerminalLayout(
remember { chatModel.terminalItems },
chatModel.terminalItems,
composeState,
sendCommand = { sendCommand(chatModel, composeState) },
close
@@ -62,7 +62,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
@Composable
fun TerminalLayout(
terminalItems: List<TerminalItem>,
terminalItems: MutableState<List<TerminalItem>>,
composeState: MutableState<ComposeState>,
sendCommand: () -> Unit,
close: () -> Unit
@@ -115,12 +115,12 @@ fun TerminalLayout(
private var lazyListState = 0 to 0
@Composable
fun TerminalLog(terminalItems: List<TerminalItem>) {
fun TerminalLog(terminalItems: MutableState<List<TerminalItem>>) {
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
DisposableEffect(Unit) {
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
}
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } }
val reversedTerminalItems by remember { derivedStateOf { terminalItems.value.reversed().toList() } }
val clipboard = LocalClipboardManager.current
LazyColumn(state = listState, reverseLayout = true) {
items(reversedTerminalItems) { item ->
@@ -152,7 +152,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
fun PreviewTerminalLayout() {
SimpleXTheme {
TerminalLayout(
terminalItems = TerminalItem.sampleData,
terminalItems = remember { mutableStateOf(TerminalItem.sampleData) },
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) },
sendCommand = {},
close = {}
@@ -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)
@@ -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.*
@@ -236,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)
@@ -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
@@ -66,7 +67,9 @@ fun SendMsgView(
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
PlatformTextField(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange)
PlatformTextField(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange) {
sendMessage(null)
}
// Disable clicks on text field
if (cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) {
Box(
@@ -320,7 +323,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 +452,8 @@ private fun SendMsgButton(
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 24.dp)
),
)
.onRightClick { onLongClick?.invoke() },
contentAlignment = Alignment.Center
) {
Icon(
@@ -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
@@ -101,7 +100,8 @@ fun CIImageView(
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = onClick
),
)
.onRightClick { showMenu.value = true },
contentScale = ContentScale.FillWidth,
)
}
@@ -118,7 +118,8 @@ fun CIImageView(
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = onClick
),
)
.onRightClick { showMenu.value = true },
contentScale = ContentScale.FillWidth,
)
}
@@ -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(
@@ -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,
)
}
@@ -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 -> {
@@ -19,8 +19,7 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.Log
import chat.simplex.common.platform.TAG
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.views.helpers.DisposableEffectOnGone
import chat.simplex.common.views.helpers.detectGesture
import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.*
val reserveTimestampStyle = SpanStyle(color = Color.Transparent)
@@ -149,7 +148,7 @@ fun MarkdownText (
} else {
ft.format.style
}
withAnnotation(tag = "URL", annotation = link) {
withAnnotation(tag = if (ft.format is Format.SimplexLink && linkMode != SimplexLinkMode.BROWSER) "SIMPLEX_URL" else "URL", annotation = link) {
withStyle(ftStyle) { append(ft.viewText(linkMode)) }
}
} else {
@@ -170,6 +169,8 @@ fun MarkdownText (
onLongClick = { offset ->
annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset)
.firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) }
annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset)
.firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) }
},
onClick = { offset ->
annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset)
@@ -182,9 +183,14 @@ fun MarkdownText (
Log.e(TAG, "Open url: ${e.stackTraceToString()}")
}
}
annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset)
.firstOrNull()?.let { annotation ->
uriHandler.openVerifiedSimplexUri(annotation.item)
}
},
shouldConsumeEvent = { offset ->
annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset).any()
annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset).any()
}
)
} else {
@@ -28,7 +28,7 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(stringResource(MR.strings.thank_you_for_installing_simplex), lineHeight = 22.sp)
ReadableTextWithLink(MR.strings.you_can_connect_to_simplex_chat_founder, simplexTeamUri)
ReadableTextWithLink(MR.strings.you_can_connect_to_simplex_chat_founder, simplexTeamUri, simplexLink = true)
Column(
Modifier.padding(top = 24.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
@@ -24,7 +24,6 @@ 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
@@ -619,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,6 +8,7 @@ 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.*
@@ -27,9 +28,8 @@ 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
@@ -130,7 +130,7 @@ private fun OnboardingButtons(openNewChatSheet: () -> Unit) {
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) {
val uriHandler = LocalUriHandler.current
ConnectButton(generalGetString(MR.strings.chat_with_developers)) {
uriHandler.openUriCatching(simplexTeamUri)
uriHandler.openVerifiedSimplexUri(simplexTeamUri)
}
Spacer(Modifier.height(DEFAULT_PADDING))
ConnectButton(generalGetString(MR.strings.tap_to_start_new_chat), openNewChatSheet)
@@ -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)
@@ -165,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
@@ -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(
@@ -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
) {
@@ -9,6 +9,7 @@ import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.ThemeOverrides
import chat.simplex.common.views.chatlist.connectIfOpenedViaUri
import chat.simplex.res.MR
import com.charleskorn.kaml.decodeFromStream
import dev.icerock.moko.resources.StringResource
@@ -281,6 +282,13 @@ inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
restore = { json.decodeFromString(it) }
)
fun UriHandler.openVerifiedSimplexUri(uri: String) {
val URI = try { URI.create(uri) } catch (e: Exception) { null }
if (URI != null) {
connectIfOpenedViaUri(URI, ChatModel)
}
}
fun UriHandler.openUriCatching(uri: String) {
try {
openUri(uri)
@@ -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
},
)
@@ -55,7 +55,7 @@ fun ReadableText(stringResId: StringResource, textAlign: TextAlign = TextAlign.S
}
@Composable
fun ReadableTextWithLink(stringResId: StringResource, link: String, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp)) {
fun ReadableTextWithLink(stringResId: StringResource, link: String, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp), simplexLink: Boolean = false) {
val annotated = annotatedStringResource(stringResId)
val primary = MaterialTheme.colors.primary
// This replaces links in text highlighted with specific color, e.g. SimplexBlue
@@ -71,7 +71,7 @@ fun ReadableTextWithLink(stringResId: StringResource, link: String, textAlign: T
newStyles
}
val uriHandler = LocalUriHandler.current
Text(AnnotatedString(annotated.text, newStyles), modifier = Modifier.padding(padding).clickable { uriHandler.openUriCatching(link) }, textAlign = textAlign, lineHeight = 22.sp)
Text(AnnotatedString(annotated.text, newStyles), modifier = Modifier.padding(padding).clickable { if (simplexLink) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openUriCatching(link) }, textAlign = textAlign, lineHeight = 22.sp)
}
@Composable
@@ -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))
@@ -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)
)
}
@@ -175,7 +175,7 @@ fun SettingsLayout(
SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openUriCatching(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary, extraPadding = true)
}
SectionDividerSpaced()
@@ -1526,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 &amp; 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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

@@ -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,10 @@ import java.io.File
val simplexWindowState = SimplexWindowState()
fun showApp() = application {
val windowState = rememberWindowState(placement = WindowPlacement.Floating, width = 1366.dp, height = 768.dp)
// 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) {
@@ -146,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 {
@@ -14,10 +14,13 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import chat.simplex.common.views.chat.*
@@ -33,7 +36,8 @@ actual fun PlatformTextField(
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
onMessageChange: (String) -> Unit
onMessageChange: (String) -> Unit,
onDone: () -> Unit,
) {
val cs = composeState.value
val focusRequester = remember { FocusRequester() }
@@ -47,11 +51,14 @@ actual fun PlatformTextField(
keyboard?.show()
}
val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) }
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) }
val textFieldValue = textFieldValueState.copy(text = cs.message)
BasicTextField(
value = cs.message,
value = textFieldValue,
onValueChange = {
if (!composeState.value.inProgress && !(composeState.value.preview is ComposePreview.VoicePreview && it != "")) {
onMessageChange(it)
if (!composeState.value.inProgress && !(composeState.value.preview is ComposePreview.VoicePreview && it.text != "")) {
textFieldValueState = it
onMessageChange(it.text)
}
},
textStyle = textStyle.value,
@@ -60,7 +67,24 @@ actual fun PlatformTextField(
capitalization = KeyboardCapitalization.Sentences,
autoCorrect = true
),
modifier = Modifier.padding(vertical = 4.dp).focusRequester(focusRequester),
modifier = Modifier
.padding(vertical = 4.dp)
.focusRequester(focusRequester)
.onPreviewKeyEvent {
if (it.key == Key.Enter && it.type == KeyEventType.KeyDown) {
if (it.isShiftPressed) {
val newText = textFieldValue.text + "\n"
textFieldValueState = textFieldValue.copy(
text = newText,
selection = TextRange(newText.length, newText.length)
)
onMessageChange(newText)
} else if (cs.message.isNotEmpty()) {
onDone()
}
true
} else false
},
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
decorationBox = { innerTextField ->
Surface(
@@ -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() {
@@ -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*/
}
}
@@ -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 =
+19 -11
View File
@@ -30,8 +30,16 @@ kotlin {
compose {
desktop {
application {
mainClass = "MainKt"
// For debugging via VisualVM
/*jvmArgs += listOf(
"-Dcom.sun.management.jmxremote.port=8080",
"-Dcom.sun.management.jmxremote.ssl=false",
"-Dcom.sun.management.jmxremote.authenticate=false"
)*/
mainClass = "chat.simplex.desktop.MainKt"
nativeDistributions {
// For debugging via VisualVM
//modules("jdk.zipfs", "jdk.management.agent")
modules("jdk.zipfs")
//includeAllModules = true
outputBaseDir.set(project.file("../release"))
@@ -40,19 +48,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"))
packageName = "SimpleX"
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"))
packageName = "SimpleX"
iconFile.set(project.file("src/jvmMain/resources/distribute/simplex.icns"))
appCategory = "public.app-category.social-networking"
bundleID = "chat.simplex.app"
}
@@ -116,7 +124,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 +134,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 +144,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 +154,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 +164,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;
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+1 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat
version: 5.2.0.4
version: 5.3.0.0
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
+1 -1
View File
@@ -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/
+1 -1
View File
@@ -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/
+1 -1
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 5.2.0.4
version: 5.3.0.0
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat