From 744c451927090288f35c4b9a19713208564b2c13 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Mar 2022 09:42:59 +0000 Subject: [PATCH] mobile: message actions (reply, share, copy) (#431) * ios: add context menu to messages * ios: UI for replies with quotes * fix: scrolling crashing in chat * ios: UI for message replies with quotes * android: UI for message replies * android: messages with quotes * android: update imports * android: refactor ChatItemView * remove comments --- apps/android/app/build.gradle | 1 + .../java/chat/simplex/app/MainActivity.kt | 5 +- .../java/chat/simplex/app/model/BGManager.kt | 2 - .../java/chat/simplex/app/model/ChatModel.kt | 46 +++++- .../java/chat/simplex/app/model/SimpleXAPI.kt | 10 +- .../chat/simplex/app/views/chat/ChatView.kt | 32 ++-- .../simplex/app/views/chat/ComposeView.kt | 14 ++ .../simplex/app/views/chat/QuotedItemView.kt | 77 +++++++++ .../simplex/app/views/chat/SendMsgView.kt | 26 +++- .../app/views/chat/item/ChatItemView.kt | 66 ++++++-- .../app/views/chat/item/EmojiItemView.kt | 42 +++++ .../app/views/chat/item/FramedItemView.kt | 146 ++++++++++++++++++ .../app/views/chat/item/TextItemView.kt | 131 ++++------------ .../app/views/chatlist/ChatPreviewView.kt | 6 +- .../chat/simplex/app/views/helpers/Share.kt | 9 +- .../app/views/usersettings/SMPServers.kt | 3 +- apps/ios/Shared/Model/ChatModel.swift | 51 +++++- apps/ios/Shared/Model/SimpleXAPI.swift | 12 +- .../Views/Chat/ChatItem/EmojiItemView.swift | 15 +- .../Views/Chat/ChatItem/FramedItemView.swift | 112 ++++++++++++++ .../Views/Chat/ChatItem/MsgContentView.swift | 97 ++++++++++++ .../Views/Chat/ChatItem/TextItemView.swift | 136 ---------------- apps/ios/Shared/Views/Chat/ChatItemView.swift | 15 +- apps/ios/Shared/Views/Chat/ChatView.swift | 35 ++++- .../Chat/ComposeMessage/ComposeView.swift | 42 +++++ .../Chat/ComposeMessage/QuotedItemView.swift | 49 ++++++ .../SendMessageView.swift | 0 .../Views/ChatList/ChatPreviewView.swift | 2 +- .../Shared/Views/Helpers/DetermineWidth.swift | 35 +++++ apps/ios/SimpleX.xcodeproj/project.pbxproj | 116 +++++++++----- .../xcshareddata/swiftpm/Package.resolved | 16 ++ cabal.project | 8 +- 32 files changed, 1005 insertions(+), 352 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/QuotedItemView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/item/EmojiItemView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt create mode 100644 apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift create mode 100644 apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift delete mode 100644 apps/ios/Shared/Views/Chat/ChatItem/TextItemView.swift create mode 100644 apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift create mode 100644 apps/ios/Shared/Views/Chat/ComposeMessage/QuotedItemView.swift rename apps/ios/Shared/Views/Chat/{ => ComposeMessage}/SendMessageView.swift (100%) create mode 100644 apps/ios/Shared/Views/Helpers/DetermineWidth.swift create mode 100644 apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 77969cc338..922b8ecbba 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -41,6 +41,7 @@ android { kotlinOptions { jvmTarget = '1.8' freeCompilerArgs += "-opt-in=kotlinx.coroutines.DelicateCoroutinesApi" + freeCompilerArgs += "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi" freeCompilerArgs += "-opt-in=androidx.compose.ui.text.ExperimentalTextApi" freeCompilerArgs += "-opt-in=androidx.compose.material.ExperimentalMaterialApi" freeCompilerArgs += "-opt-in=com.google.accompanist.insets.ExperimentalAnimatedInsets" diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 20fff5ec83..18288bc4f1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -112,9 +112,8 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) { } //fun testJson() { -// val str = """ -// {} +// val str: String = """ // """.trimIndent() // -// println(json.decodeFromString(str)) +// println(json.decodeFromString(str)) //} diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/BGManager.kt b/apps/android/app/src/main/java/chat/simplex/app/model/BGManager.kt index d274a98f6d..9894713752 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/BGManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/BGManager.kt @@ -4,10 +4,8 @@ import android.content.Context import android.util.Log import androidx.work.* import chat.simplex.app.TAG -import chat.simplex.app.chatRecvMsg import kotlinx.datetime.Clock import java.time.Duration -import java.util.concurrent.TimeUnit class BGManager(appContext: Context, workerParams: WorkerParameters): //, ctrl: ChatCtrl): Worker(appContext, workerParams) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 7ccb1bad4e..d91262a249 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -466,24 +466,31 @@ data class ChatItem ( val chatDir: CIDirection, val meta: CIMeta, val content: CIContent, - val formattedText: List? = null + val formattedText: List? = null, + val quotedItem: CIQuote? = null ) { val id: Long get() = meta.itemId val timestampText: String get() = meta.timestampText val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew + val memberDisplayName: String? get() = + if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.memberProfile.displayName + else null + companion object { fun getSampleData( id: Long = 1, dir: CIDirection = CIDirection.DirectSnd(), ts: Instant = Clock.System.now(), text: String = "hello\nthere", - status: CIStatus = CIStatus.SndNew() + status: CIStatus = CIStatus.SndNew(), + quotedItem: CIQuote? = null ) = ChatItem( chatDir = dir, meta = CIMeta.getSample(id, ts, text, status), - content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)) + content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)), + quotedItem = quotedItem ) } } @@ -566,9 +573,13 @@ sealed class CIStatus { class RcvRead: CIStatus() } +interface ItemContent { + val text: String +} + @Serializable -sealed class CIContent { - abstract val text: String +sealed class CIContent: ItemContent { + abstract override val text: String @Serializable @SerialName("sndMsgContent") class SndMsgContent(val msgContent: MsgContent): CIContent() { @@ -591,6 +602,31 @@ sealed class CIContent { } } +@Serializable +class CIQuote ( + val chatDir: CIDirection? = null, + val itemId: Long? = null, + val sharedMsgId: String? = null, + val sentAt: Instant, + val content: MsgContent, + val formattedText: List? = null +): ItemContent { + override val text: String get() = content.text + + fun sender(user: User): String? = when (chatDir) { + is CIDirection.DirectSnd -> "you" + is CIDirection.DirectRcv -> null + is CIDirection.GroupSnd -> user.displayName + is CIDirection.GroupRcv -> chatDir.groupMember.memberProfile.displayName + null -> null + } + + companion object { + fun getSample(itemId: Long?, sentAt: Instant, text: String, chatDir: CIDirection?): CIQuote = + CIQuote(chatDir = chatDir, itemId = itemId, sentAt = sentAt, content = MsgContent.MCText(text)) + } +} + @Serializable(with = MsgContentSerializer::class) sealed class MsgContent { abstract val text: String diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 147cf888b0..820a955304 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -122,8 +122,10 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap return null } - suspend fun apiSendMessage(type: ChatType, id: Long, mc: MsgContent): AChatItem? { - val r = sendCmd(CC.ApiSendMessage(type, id, mc)) + suspend fun apiSendMessage(type: ChatType, id: Long, quotedItemId: Long? = null, mc: MsgContent): AChatItem? { + val cmd = if (quotedItemId == null) CC.ApiSendMessage(type, id, mc) + else CC.ApiSendMessageQuote(type, id, quotedItemId, mc) + val r = sendCmd(cmd) if (r is CR.NewChatItem ) return r.chatItem Log.e(TAG, "apiSendMessage bad response: ${r.responseType} ${r.details}") return null @@ -343,6 +345,7 @@ sealed class CC { class ApiGetChats: CC() class ApiGetChat(val type: ChatType, val id: Long): CC() class ApiSendMessage(val type: ChatType, val id: Long, val mc: MsgContent): CC() + class ApiSendMessageQuote(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent): CC() class GetUserSMPServers(): CC() class SetUserSMPServers(val smpServers: List): CC() class AddContact: CC() @@ -364,6 +367,7 @@ sealed class CC { is ApiGetChats -> "/_get chats" is ApiGetChat -> "/_get chat ${chatRef(type, id)} count=100" is ApiSendMessage -> "/_send ${chatRef(type, id)} ${mc.cmdString}" + is ApiSendMessageQuote -> "/_send_quote ${chatRef(type, id)} $itemId ${mc.cmdString}" is GetUserSMPServers -> "/smp_servers" is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}" is AddContact -> "/connect" @@ -386,6 +390,7 @@ sealed class CC { is ApiGetChats -> "apiGetChats" is ApiGetChat -> "apiGetChat" is ApiSendMessage -> "apiSendMessage" + is ApiSendMessageQuote -> "apiSendMessageQuote" is GetUserSMPServers -> "getUserSMPServers" is SetUserSMPServers -> "setUserSMPServers" is AddContact -> "addContact" @@ -422,6 +427,7 @@ class APIResponse(val resp: CR, val corr: String? = null) { json.decodeFromString(str) } catch(e: Exception) { try { + Log.d(TAG, e.localizedMessage) val data = json.parseToJsonElement(str).jsonObject APIResponse( resp = CR.Response(data["resp"]!!.jsonObject["type"]?.toString() ?: "invalid", json.encodeToString(data)), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index b5061bb55b..0d0e2b146d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -3,18 +3,19 @@ package chat.simplex.app.views.chat import android.content.res.Configuration import android.util.Log import androidx.activity.compose.BackHandler -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.ArrowBack +import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.mapSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -22,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.TAG import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.item.ChatItemView import chat.simplex.app.views.helpers.* @@ -35,9 +37,11 @@ import kotlinx.datetime.Clock @Composable fun ChatView(chatModel: ChatModel) { val chat: Chat? = chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value } - if (chat == null) { + val user = chatModel.currentUser.value + if (chat == null || user == null) { chatModel.chatId.value = null } else { + val quotedItem = remember { mutableStateOf(null) } BackHandler { chatModel.chatId.value = null } // TODO a more advanced version would mark as read only if in view LaunchedEffect(chat.chatItems) { @@ -54,7 +58,7 @@ fun ChatView(chatModel: ChatModel) { } } } - ChatLayout(chat, chatModel.chatItems, + ChatLayout(user, chat, chatModel.chatItems, quotedItem, back = { chatModel.chatId.value = null }, info = { ModalManager.shared.showCustomModal { close -> ChatInfoView(chatModel, close) } }, sendMessage = { msg -> @@ -64,8 +68,10 @@ fun ChatView(chatModel: ChatModel) { val newItem = chatModel.controller.apiSendMessage( type = cInfo.chatType, id = cInfo.apiId, + quotedItemId = quotedItem.value?.meta?.itemId, mc = MsgContent.MCText(msg) ) + quotedItem.value = null // hide "in progress" if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem) } @@ -76,7 +82,10 @@ fun ChatView(chatModel: ChatModel) { @Composable fun ChatLayout( - chat: Chat, chatItems: List, + user: User, + chat: Chat, + chatItems: List, + quotedItem: MutableState, back: () -> Unit, info: () -> Unit, sendMessage: (String) -> Unit @@ -88,11 +97,11 @@ fun ChatLayout( ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { Scaffold( topBar = { ChatInfoToolbar(chat, back, info) }, - bottomBar = { SendMsgView(sendMessage) }, + bottomBar = { ComposeView(quotedItem, sendMessage) }, modifier = Modifier.navigationBarsWithImePadding() ) { contentPadding -> Box(Modifier.padding(contentPadding)) { - ChatItemsList(chatItems) + ChatItemsList(user, chatItems, quotedItem) } } } @@ -152,7 +161,7 @@ val CIListStateSaver = run { } @Composable -fun ChatItemsList(chatItems: List) { +fun ChatItemsList(user: User, chatItems: List, quotedItem: MutableState) { val listState = rememberLazyListState() val keyboardState by getKeyboardState() val ciListState = rememberSaveable(stateSaver = CIListStateSaver) { @@ -160,9 +169,10 @@ fun ChatItemsList(chatItems: List) { } val scope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current + val cxt = LocalContext.current LazyColumn(state = listState) { items(chatItems) { cItem -> - ChatItemView(cItem, uriHandler) + ChatItemView(user, cItem, quotedItem, cxt, uriHandler) } val len = chatItems.count() if (len > 1 && (keyboardState != ciListState.value.keyboardState || !ciListState.value.scrolled || len != ciListState.value.itemCount)) { @@ -201,12 +211,14 @@ fun PreviewChatLayout() { ) ) ChatLayout( + user = User.sampleData, chat = Chat( chatInfo = ChatInfo.Direct.sampleData, chatItems = chatItems, chatStats = Chat.ChatStats() ), chatItems = chatItems, + quotedItem = remember { mutableStateOf(null) }, back = {}, info = {}, sendMessage = {} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt new file mode 100644 index 0000000000..ebea19ef94 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt @@ -0,0 +1,14 @@ +package chat.simplex.app.views.chat + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import chat.simplex.app.model.ChatItem + +@Composable +fun ComposeView(quotedItem: MutableState, sendMessage: (String) -> Unit) { + Column { + QuotedItemView(quotedItem) + SendMsgView(sendMessage) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/QuotedItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/QuotedItemView.kt new file mode 100644 index 0000000000..5f8f1ac54f --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/QuotedItemView.kt @@ -0,0 +1,77 @@ +package chat.simplex.app.views.chat + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.model.CIDirection +import chat.simplex.app.model.ChatItem +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.chat.item.* +import kotlinx.datetime.Clock + +@Composable +fun QuotedItemView(quotedItem: MutableState) { + val qi = quotedItem.value + if (qi != null) { + val sent = qi.chatDir.sent + Row( + Modifier.padding(top = 8.dp) + .background(if (sent) SentColorLight else ReceivedColorLight), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + Modifier.padding(start = 16.dp) + .padding(vertical = 12.dp) + .fillMaxWidth() + .weight(1F) + ) { + QuoteText(qi) + } + IconButton(onClick = { quotedItem.value = null }) { + Icon( + Icons.Outlined.Close, + "Remove quote", + tint = MaterialTheme.colors.primary, + modifier = Modifier.padding(10.dp) + ) + } + } + } +} + +@Composable +private fun QuoteText(qi: ChatItem) { + val member = qi.memberDisplayName + if (member == null) { + Text(qi.content.text, maxLines = 3) + } else { + val annotatedText = buildAnnotatedString { + withStyle(boldFont) { append(member) } + append(": ${qi.content.text}") + } + Text(annotatedText, maxLines = 3) + } +} + +@Preview +@Composable +fun PreviewTextItemViewEmoji() { + SimpleXTheme { + QuotedItemView( + quotedItem = remember { + mutableStateOf(ChatItem.getSampleData( + 1, CIDirection.DirectRcv(), Clock.System.now(), "hello" + )) + } + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt index 639933dde5..504d7552e2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt @@ -21,14 +21,24 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.chat.item.* @Composable fun SendMsgView(sendMessage: (String) -> Unit) { - var cmd by remember { mutableStateOf("") } + var msg by remember { mutableStateOf("") } + val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) + var textStyle by remember { mutableStateOf(smallFont) } BasicTextField( - value = cmd, - onValueChange = { cmd = it }, - textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground), + value = msg, + onValueChange = { + msg = it + textStyle = if(isShortEmoji(it)) { + if (it.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont + } else { + smallFont + } + }, + textStyle = textStyle, maxLines = 16, keyboardOptions = KeyboardOptions.Default.copy( capitalization = KeyboardCapitalization.Sentences, @@ -54,7 +64,7 @@ fun SendMsgView(sendMessage: (String) -> Unit) { ) { innerTextField() } - val color = if (cmd.isNotEmpty()) MaterialTheme.colors.primary else Color.Gray + val color = if (msg.isNotEmpty()) MaterialTheme.colors.primary else Color.Gray Icon( Icons.Outlined.ArrowUpward, "Send Message", @@ -65,9 +75,9 @@ fun SendMsgView(sendMessage: (String) -> Unit) { .clip(CircleShape) .background(color) .clickable { - if (cmd.isNotEmpty()) { - sendMessage(cmd) - cmd = "" + if (msg.isNotEmpty()) { + sendMessage(msg) + msg = "" } } ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index ddcbbdc1a5..acc249bdf7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -1,33 +1,74 @@ package chat.simplex.app.views.chat.item +import android.content.Context +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import chat.simplex.app.model.CIDirection -import chat.simplex.app.model.ChatItem +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.helpers.copyText +import chat.simplex.app.views.helpers.shareText import kotlinx.datetime.Clock @Composable -fun ChatItemView(chatItem: ChatItem, uriHandler: UriHandler? = null) { - val sent = chatItem.chatDir.sent +fun ChatItemView(user: User, cItem: ChatItem, quotedItem: MutableState, cxt: Context, uriHandler: UriHandler? = null) { + val sent = cItem.chatDir.sent val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart - + var showMenu by remember { mutableStateOf(false) } Box( modifier = Modifier .padding(bottom = 4.dp) .fillMaxWidth() .padding( - start = if (sent) 60.dp else 16.dp, - end = if (sent) 16.dp else 60.dp, + start = if (sent) 86.dp else 16.dp, + end = if (sent) 16.dp else 86.dp, ), contentAlignment = alignment, ) { - TextItemView(chatItem, uriHandler) + Column(Modifier.combinedClickable(onLongClick = { showMenu = true }, onClick = {})) { + if (cItem.quotedItem == null && isShortEmoji(cItem.content.text)) { + EmojiItemView(cItem) + } else { + FramedItemView(user, cItem, uriHandler) + } + DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { + ItemAction("Reply", Icons.Outlined.Reply, onClick = { + quotedItem.value = cItem + showMenu = false + }) + ItemAction("Share", Icons.Outlined.Share, onClick = { + shareText(cxt, cItem.content.text) + showMenu = false + }) + ItemAction("Copy", Icons.Outlined.ContentCopy, onClick = { + copyText(cxt, cItem.content.text) + showMenu = false + }) + } + } + } +} + +@Composable +private fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit) { + DropdownMenuItem(onClick) { + Row { + Text(text, modifier = Modifier + .fillMaxWidth() + .weight(1F)) + Icon(icon, text, tint = HighOrLowlight) + } } } @@ -36,9 +77,12 @@ fun ChatItemView(chatItem: ChatItem, uriHandler: UriHandler? = null) { fun PreviewChatItemView() { SimpleXTheme { ChatItemView( - chatItem = ChatItem.getSampleData( + User.sampleData, + ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" - ) + ), + quotedItem = remember { mutableStateOf(null) }, + cxt = LocalContext.current ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/EmojiItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/EmojiItemView.kt new file mode 100644 index 0000000000..8d6e59edc1 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/EmojiItemView.kt @@ -0,0 +1,42 @@ +package chat.simplex.app.views.chat.item + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.app.model.ChatItem + +val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp) +val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp) + +@Composable +fun EmojiItemView(chatItem: ChatItem) { + Column( + Modifier.padding(vertical = 8.dp, horizontal = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + EmojiText(chatItem.content.text) + CIMetaView(chatItem) + } +} + +@Composable +fun EmojiText(text: String) { + val s = text.trim() + Text(s, style = if (s.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont) +} + +private fun isSimpleEmoji(c: Int): Boolean = c > 0x238C + +fun isEmoji(c: Int): Boolean = isSimpleEmoji(c) // || isCombinedIntoEmoji(c) + +// TODO count perceived emojis, possibly using icu4j +fun isShortEmoji(str: String): Boolean { + val s = str.trim() + return s.codePoints().count() in 1..5 && s.codePoints().allMatch(::isEmoji) +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt new file mode 100644 index 0000000000..abdc15effc --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt @@ -0,0 +1,146 @@ +package chat.simplex.app.views.chat.item + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.UriHandler +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleXTheme +import kotlinx.datetime.Clock + +val SentColorLight = Color(0x1E45B8FF) +val ReceivedColorLight = Color(0x20B1B0B5) +val SentQuoteColorLight = Color(0x2545B8FF) +val ReceivedQuoteColorLight = Color(0x25B1B0B5) + +@Composable +fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null) { + val sent = ci.chatDir.sent + Surface( + shape = RoundedCornerShape(18.dp), + color = if (sent) SentColorLight else ReceivedColorLight + ) { + Box(contentAlignment = Alignment.BottomEnd) { + Column(Modifier.width(IntrinsicSize.Max)) { + val qi = ci.quotedItem + if (qi != null) { + Box( + Modifier + .background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight) + .padding(vertical = 6.dp, horizontal = 12.dp) + .fillMaxWidth() + ) { + MarkdownText( + qi, sender = qi.sender(user), senderBold = true, maxLines = 3, + style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface) + ) + } + } + Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { + if (ci.formattedText == null && isShortEmoji(ci.content.text)) { + Column( + Modifier.padding(bottom = 2.dp).fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + EmojiText(ci.content.text) + Text("") + } + } else { + MarkdownText( + ci.content, ci.formattedText, ci.memberDisplayName, + metaText = ci.timestampText, uriHandler = uriHandler, senderBold = true + ) + } + } + } + Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) { + CIMetaView(ci) + } + } + } +} + +@Preview +@Composable +fun PreviewTextItemViewSnd() { + SimpleXTheme { + FramedItemView( + User.sampleData, + ChatItem.getSampleData( + 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ) + ) + } +} + +@Preview +@Composable +fun PreviewTextItemViewRcv() { + SimpleXTheme { + FramedItemView( + User.sampleData, + ChatItem.getSampleData( + 1, CIDirection.DirectRcv(), Clock.System.now(), "hello" + ) + ) + } +} + +@Preview +@Composable +fun PreviewTextItemViewLong() { + SimpleXTheme { + FramedItemView( + User.sampleData, + ChatItem.getSampleData( + 1, + CIDirection.DirectSnd(), + Clock.System.now(), + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + ) + ) + } +} + +@Preview +@Composable +fun PreviewTextItemViewQuote() { + SimpleXTheme { + FramedItemView( + User.sampleData, + ChatItem.getSampleData( + 1, CIDirection.DirectSnd(), + Clock.System.now(), + "https://simplex.chat", + CIStatus.SndSent(), + quotedItem = CIQuote.getSample(1, Clock.System.now(), "hi", chatDir = CIDirection.DirectRcv()) + ) + ) + } +} + +@Preview +@Composable +fun PreviewTextItemViewEmoji() { + SimpleXTheme { + FramedItemView( + User.sampleData, + ChatItem.getSampleData( + 1, CIDirection.DirectSnd(), + Clock.System.now(), + "👍", + CIStatus.SndSent(), + quotedItem = CIQuote.getSample(1, Clock.System.now(), "Lorem ipsum dolor sit amet", chatDir = CIDirection.DirectRcv()) + ) + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt index d21d0749ee..6789833fe2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt @@ -1,48 +1,17 @@ package chat.simplex.app.views.chat.item -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.model.CIDirection -import chat.simplex.app.model.ChatItem -import chat.simplex.app.ui.theme.SimpleXTheme -import kotlinx.datetime.Clock - -// TODO move to theme -val SentColorLight = Color(0x1E45B8FF) -val ReceivedColorLight = Color(0x1EB1B0B5) - -@Composable -fun TextItemView(chatItem: ChatItem, uriHandler: UriHandler? = null) { - val sent = chatItem.chatDir.sent - Surface( - shape = RoundedCornerShape(18.dp), - color = if (sent) SentColorLight else ReceivedColorLight - ) { - Box( - modifier = Modifier.padding(vertical = 6.dp, horizontal = 12.dp) - ) { - Box(contentAlignment = Alignment.BottomEnd) { - MarkdownText(chatItem, uriHandler = uriHandler, groupMemberBold = true) - CIMetaView(chatItem) - } - } - } -} +import chat.simplex.app.model.* val reserveTimestampStyle = SpanStyle(color = Color.Transparent) val boldFont = SpanStyle(fontWeight = FontWeight.Medium) @@ -56,33 +25,44 @@ fun appendGroupMember(b: AnnotatedString.Builder, chatItem: ChatItem, groupMembe } } +fun appendSender(b: AnnotatedString.Builder, sender: String?, senderBold: Boolean) { + if (sender != null) { + if (senderBold) b.withStyle(boldFont) { append(sender) } + else b.append(sender) + b.append(": ") + } +} + @Composable fun MarkdownText ( - chatItem: ChatItem, + content: ItemContent, + formattedText: List? = null, + sender: String? = null, + metaText: String? = null, style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp), maxLines: Int = Int.MAX_VALUE, overflow: TextOverflow = TextOverflow.Clip, uriHandler: UriHandler? = null, - groupMemberBold: Boolean = false, + senderBold: Boolean = false, modifier: Modifier = Modifier ) { - if (chatItem.formattedText == null) { + if (formattedText == null) { val annotatedText = buildAnnotatedString { - appendGroupMember(this, chatItem, groupMemberBold) - append(chatItem.content.text) - withStyle(reserveTimestampStyle) { append(" ${chatItem.timestampText}") } - } - SelectionContainer { - Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow) + appendSender(this, sender, senderBold) + append(content.text) + if (metaText != null) withStyle(reserveTimestampStyle) { append(" $metaText") } } + Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow) } else { + var hasLinks = false val annotatedText = buildAnnotatedString { - appendGroupMember(this, chatItem, groupMemberBold) - for (ft in chatItem.formattedText) { + appendSender(this, sender, senderBold) + for (ft in formattedText) { if (ft.format == null) append(ft.text) else { val link = ft.link if (link != null) { + hasLinks = true withAnnotation(tag = "URL", annotation = link) { withStyle(ft.format.style) { append(ft.text) } } @@ -91,60 +71,17 @@ fun MarkdownText ( } } } - withStyle(reserveTimestampStyle) { append(" ${chatItem.timestampText}") } + if (metaText != null) withStyle(reserveTimestampStyle) { append(" $metaText") } } - if (uriHandler != null) { - SelectionContainer { - ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, - onClick = { offset -> - annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> uriHandler.openUri(annotation.item) } - } - ) - } + if (hasLinks && uriHandler != null) { + ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, + onClick = { offset -> + annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) + .firstOrNull()?.let { annotation -> uriHandler.openUri(annotation.item) } + } + ) } else { - SelectionContainer { - Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow) - } + Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow) } } } - -@Preview -@Composable -fun PreviewTextItemViewSnd() { - SimpleXTheme { - TextItemView( - chatItem = ChatItem.getSampleData( - 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" - ) - ) - } -} - -@Preview -@Composable -fun PreviewTextItemViewRcv() { - SimpleXTheme { - TextItemView( - chatItem = ChatItem.getSampleData( - 1, CIDirection.DirectRcv(), Clock.System.now(), "hello" - ) - ) - } -} - -@Preview -@Composable -fun PreviewTextItemViewLong() { - SimpleXTheme { - TextItemView( - chatItem = ChatItem.getSampleData( - 1, - CIDirection.DirectSnd(), - Clock.System.now(), - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - ) - ) - } -} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 95fc05f7c2..45f7fe5322 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -39,9 +39,11 @@ fun ChatPreviewView(chat: Chat) { fontWeight = FontWeight.Bold ) - if (chat.chatItems.count() > 0) { + val ci = chat.chatItems.lastOrNull() + if (ci != null) { MarkdownText( - chat.chatItems.last(), + ci.content, ci.formattedText, ci.memberDisplayName, + metaText = ci.timestampText, maxLines = 2, overflow = TextOverflow.Ellipsis ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt index be51412768..5cac836f14 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt @@ -1,7 +1,7 @@ package chat.simplex.app.views.helpers -import android.content.Context -import android.content.Intent +import android.content.* +import androidx.core.content.ContextCompat fun shareText(cxt: Context, text: String) { val sendIntent: Intent = Intent().apply { @@ -12,3 +12,8 @@ fun shareText(cxt: Context, text: String) { val shareIntent = Intent.createChooser(sendIntent, null) cxt.startActivity(shareIntent) } + +fun copyText(cxt: Context, text: String) { + val clipboard = ContextCompat.getSystemService(cxt, ClipboardManager::class.java) + clipboard?.setPrimaryClip(ClipData.newPlainText("text", text)) +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServers.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServers.kt index 548762886b..8c593e1fb0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServers.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SMPServers.kt @@ -21,7 +21,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.model.ChatModel -import chat.simplex.app.ui.theme.* +import chat.simplex.app.ui.theme.HighOrLowlight +import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.helpers.AlertManager import chat.simplex.app.views.helpers.withApi diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index f38c94f5e0..c553543b3a 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -527,7 +527,8 @@ struct ChatItem: Identifiable, Decodable { var meta: CIMeta var content: CIContent var formattedText: [FormattedText]? - + var quotedItem: CIQuote? + var id: Int64 { get { meta.itemId } } var timestampText: Text { get { meta.timestampText } } @@ -537,11 +538,22 @@ struct ChatItem: Identifiable, Decodable { return false } - static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew) -> ChatItem { + var memberDisplayName: String? { + get { + if case let .groupRcv(groupMember) = chatDir { + return groupMember.memberProfile.displayName + } else { + return nil + } + } + } + + static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil) -> ChatItem { ChatItem( chatDir: dir, meta: CIMeta.getSample(id, ts, text, status), - content: .sndMsgContent(msgContent: .text(text)) + content: .sndMsgContent(msgContent: .text(text)), + quotedItem: quotedItem ) } } @@ -603,7 +615,11 @@ enum CIStatus: Decodable { case rcvRead } -enum CIContent: Decodable { +protocol ItemContent { + var text: String { get } +} + +enum CIContent: Decodable, ItemContent { case sndMsgContent(msgContent: MsgContent) case rcvMsgContent(msgContent: MsgContent) case sndFileInvitation(fileId: Int64, filePath: String) @@ -625,6 +641,33 @@ struct RcvFileTransfer: Decodable { } +struct CIQuote: Decodable, ItemContent { + var chatDir: CIDirection? + var itemId: Int64? + var sharedMsgId: String? = nil + var sentAt: Date + var content: MsgContent + var formattedText: [FormattedText]? + + var text: String { get { content.text } } + + var sender: String? { + get { + switch (chatDir) { + case .directSnd: return "you" + case .directRcv: return nil + case .groupSnd: return ChatModel.shared.currentUser?.displayName + case let .groupRcv(member): return member.memberProfile.displayName + case nil: return nil + } + } + } + + static func getSample(_ itemId: Int64?, _ sentAt: Date, _ text: String, chatDir: CIDirection?) -> CIQuote { + CIQuote(chatDir: chatDir, itemId: itemId, sentAt: sentAt, content: .text(text)) + } +} + enum MsgContent { case text(String) // TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 0893c1c8a3..c7111a3541 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -22,6 +22,7 @@ enum ChatCommand { case apiGetChats case apiGetChat(type: ChatType, id: Int64) case apiSendMessage(type: ChatType, id: Int64, msg: MsgContent) + case apiSendMessageQuote(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) case getUserSMPServers case setUserSMPServers(smpServers: [String]) case addContact @@ -45,6 +46,7 @@ enum ChatCommand { case .apiGetChats: return "/_get chats" case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100" case let .apiSendMessage(type, id, mc): return "/_send \(ref(type, id)) \(mc.cmdString)" + case let .apiSendMessageQuote(type, id, itemId, mc): return "/_send_quote \(ref(type, id)) \(itemId) \(mc.cmdString)" case .getUserSMPServers: return "/smp_servers" case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))" case .addContact: return "/connect" @@ -71,6 +73,7 @@ enum ChatCommand { case .apiGetChats: return "apiGetChats" case .apiGetChat: return "apiGetChat" case .apiSendMessage: return "apiSendMessage" + case .apiSendMessageQuote: return "apiSendMessageQuote" case .getUserSMPServers: return "getUserSMPServers" case .setUserSMPServers: return "setUserSMPServers" case .addContact: return "addContact" @@ -362,9 +365,14 @@ func apiGetChat(type: ChatType, id: Int64) async throws -> Chat { throw r } -func apiSendMessage(type: ChatType, id: Int64, msg: MsgContent) async throws -> ChatItem { +func apiSendMessage(type: ChatType, id: Int64, quotedItemId: Int64?, msg: MsgContent) async throws -> ChatItem { let chatModel = ChatModel.shared - let cmd = ChatCommand.apiSendMessage(type: type, id: id, msg: msg) + let cmd: ChatCommand + if let itemId = quotedItemId { + cmd = .apiSendMessageQuote(type: type, id: id, itemId: itemId, msg: msg) + } else { + cmd = .apiSendMessage(type: type, id: id, msg: msg) + } let r: ChatResponse if type == .direct { var cItem: ChatItem! diff --git a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift index 7a4ff3b5f2..260e6e9fe6 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift @@ -12,25 +12,22 @@ struct EmojiItemView: View { var chatItem: ChatItem var body: some View { - let sent = chatItem.chatDir.sent - let s = chatItem.content.text.trimmingCharacters(in: .whitespaces) - VStack(spacing: 1) { - Text(s) - .font(s.count < 4 ? largeEmojiFont : mediumEmojiFont) + emojiText(chatItem.content.text) .padding(.top, 8) .padding(.horizontal, 6) - .frame(maxWidth: .infinity, alignment: sent ? .trailing : .leading) CIMetaView(chatItem: chatItem) .padding(.bottom, 8) .padding(.horizontal, 12) - .frame(maxWidth: .infinity, alignment: sent ? .trailing : .leading) } - .padding(.horizontal) - .frame(maxWidth: .infinity, alignment: sent ? .trailing : .leading) } } +func emojiText(_ text: String) -> Text { + let s = text.trimmingCharacters(in: .whitespaces) + return Text(s).font(s.count < 4 ? largeEmojiFont : mediumEmojiFont) +} + struct EmojiItemView_Previews: PreviewProvider { static var previews: some View { Group{ diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift new file mode 100644 index 0000000000..90e5d4e2a6 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -0,0 +1,112 @@ +// +// FramedItemView.swift +// SimpleX +// +// Created by Evgeny Poberezkin on 04/02/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +private let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12) +private let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17) +private let sentQuoteColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.11) +private let sentQuoteColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.09) + +struct FramedItemView: View { + @Environment(\.colorScheme) var colorScheme + var chatItem: ChatItem + @State var msgWidth: CGFloat = 0 + + var body: some View { + let v = ZStack(alignment: .bottomTrailing) { + VStack(alignment: .leading, spacing: 0) { + if let qi = chatItem.quotedItem { + MsgContentView( + content: qi, + sender: qi.sender + ) + .lineLimit(3) + .font(.subheadline) + .padding(.vertical, 6) + .padding(.horizontal, 12) + .frame(minWidth: msgWidth, alignment: .leading) + .background( + chatItem.chatDir.sent + ? (colorScheme == .light ? sentQuoteColorLight : sentQuoteColorDark) + : Color(uiColor: .quaternarySystemFill) + ) + .overlay(DetermineWidth()) + } + + if chatItem.formattedText == nil && isShortEmoji(chatItem.content.text) { + VStack { + emojiText(chatItem.content.text) + Text("") + } + .padding(.vertical, 6) + .padding(.horizontal, 12) + .overlay(DetermineWidth()) + .frame(minWidth: msgWidth, alignment: .center) + .padding(.bottom, 2) + } else { + MsgContentView( + content: chatItem.content, + formattedText: chatItem.formattedText, + sender: chatItem.memberDisplayName, + metaText: chatItem.timestampText + ) + .padding(.vertical, 6) + .padding(.horizontal, 12) + .overlay(DetermineWidth()) + .frame(minWidth: 0, alignment: .leading) + .textSelection(.enabled) + } + } + .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } + + CIMetaView(chatItem: chatItem) + .padding(.trailing, 12) + .padding(.bottom, 6) + } + .background(chatItemFrameColor(chatItem, colorScheme)) + .cornerRadius(18) + + switch chatItem.meta.itemStatus { + case .sndErrorAuth: + v.onTapGesture { msgDeliveryError("Most likely this contact has deleted the connection with you.") } + case let .sndError(agentError): + v.onTapGesture { msgDeliveryError("Unexpected error: \(String(describing: agentError))") } + default: v + } + } + + private func msgDeliveryError(_ err: String) { + AlertManager.shared.showAlertMsg( + title: "Message delivery error", + message: err + ) + } +} + +func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { + ci.chatDir.sent + ? (colorScheme == .light ? sentColorLight : sentColorDark) + : Color(uiColor: .tertiarySystemGroupedBackground) +} + +struct FramedItemView_Previews: PreviewProvider { + static var previews: some View { + Group{ + FramedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello")) + FramedItemView(chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd))) + FramedItemView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv))) + FramedItemView(chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv))) + FramedItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -")) + FramedItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ")) + FramedItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat")) + FramedItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat")) + } + .previewLayout(.fixed(width: 360, height: 200)) + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift new file mode 100644 index 0000000000..4620fb34c8 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -0,0 +1,97 @@ +// +// MsgContentView.swift +// SimpleX +// +// Created by Evgeny on 13/03/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1) +private let linkColor = Color(uiColor: uiLinkColor) + +struct MsgContentView: View { + var content: ItemContent + var formattedText: [FormattedText]? = nil + var sender: String? = nil + var metaText: Text? = nil + + var body: some View { + let v = messageText(content, formattedText, sender) + if let mt = metaText { + return v + reserveSpaceForMeta(mt) + } else { + return v + } + } + + private func reserveSpaceForMeta(_ meta: Text) -> Text { + (Text(" ") + meta) + .font(.caption) + .foregroundColor(.clear) + } +} + +func messageText(_ content: ItemContent, _ formattedText: [FormattedText]?, _ sender: String?, preview: Bool = false) -> Text { + let s = content.text + var res: Text + if let ft = formattedText, ft.count > 0 { + res = formattText(ft[0], preview) + var i = 1 + while i < ft.count { + res = res + formattText(ft[i], preview) + i = i + 1 + } + } else { + res = Text(s) + } + + if let s = sender { + let t = Text(s) + return (preview ? t : t.fontWeight(.medium)) + Text(": ") + res + } else { + return res + } +} + +private func formattText(_ ft: FormattedText, _ preview: Bool) -> Text { + let t = ft.text + if let f = ft.format { + switch (f) { + case .bold: return Text(t).bold() + case .italic: return Text(t).italic() + case .strikeThrough: return Text(t).strikethrough() + case .snippet: return Text(t).font(.body.monospaced()) + case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary) + case let .colored(color): return Text(t).foregroundColor(color.uiColor) + case .uri: return linkText(t, t, preview, prefix: "") + case .email: return linkText(t, t, preview, prefix: "mailto:") + case .phone: return linkText(t, t.replacingOccurrences(of: " ", with: ""), preview, prefix: "tel:") + } + } else { + return Text(t) + } +} + +private func linkText(_ s: String, _ link: String, + _ preview: Bool, prefix: String) -> Text { + preview + ? Text(s).foregroundColor(linkColor).underline(color: linkColor) + : Text(AttributedString(s, attributes: AttributeContainer([ + .link: NSURL(string: prefix + link) as Any, + .foregroundColor: uiLinkColor as Any + ]))).underline() +} + +struct MsgContentView_Previews: PreviewProvider { + static var previews: some View { + let chatItem = ChatItem.getSample(1, .directSnd, .now, "hello") + return MsgContentView( + content: chatItem.content, + formattedText: chatItem.formattedText, + sender: chatItem.memberDisplayName, + metaText: chatItem.timestampText + ) + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatItem/TextItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/TextItemView.swift deleted file mode 100644 index 4a79c1f423..0000000000 --- a/apps/ios/Shared/Views/Chat/ChatItem/TextItemView.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// TextItemView.swift -// SimpleX -// -// Created by Evgeny Poberezkin on 04/02/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI - -private let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12) -private let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17) -private let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1) -private let linkColor = Color(uiColor: uiLinkColor) - -struct TextItemView: View { - @Environment(\.colorScheme) var colorScheme - var chatItem: ChatItem - var width: CGFloat - private let codeFont = Font.custom("Courier", size: UIFont.preferredFont(forTextStyle: .body).pointSize) - - var body: some View { - let sent = chatItem.chatDir.sent - let maxWidth = width * 0.78 - - return ZStack(alignment: .bottomTrailing) { - (messageText(chatItem) + reserveSpaceForMeta(chatItem.timestampText)) - .padding(.vertical, 6) - .padding(.horizontal, 12) - .frame(minWidth: 0, alignment: .leading) - .textSelection(.enabled) - - CIMetaView(chatItem: chatItem) - .padding(.trailing, 12) - .padding(.bottom, 6) - } - .background( - sent - ? (colorScheme == .light ? sentColorLight : sentColorDark) - : Color(uiColor: .tertiarySystemGroupedBackground) - ) - .cornerRadius(18) - .padding(.horizontal) - .frame( - maxWidth: maxWidth, - maxHeight: .infinity, - alignment: sent ? .trailing : .leading - ) - .onTapGesture { - switch chatItem.meta.itemStatus { - case .sndErrorAuth: msgDeliveryError("Most likely this contact has deleted the connection with you.") - case let .sndError(agentError): msgDeliveryError("Unexpected error: \(String(describing: agentError))") - default: return - } - } - } - - private func reserveSpaceForMeta(_ meta: Text) -> Text { - (Text(" ") + meta) - .font(.caption) - .foregroundColor(.clear) - } - - private func msgDeliveryError(_ err: String) { - AlertManager.shared.showAlertMsg( - title: "Message delivery error", - message: err - ) - } -} - -func messageText(_ chatItem: ChatItem, preview: Bool = false) -> Text { - let s = chatItem.content.text - var res: Text - if let ft = chatItem.formattedText, ft.count > 0 { - res = formattedText(ft[0], preview) - var i = 1 - while i < ft.count { - res = res + formattedText(ft[i], preview) - i = i + 1 - } - } else { - res = Text(s) - } - - if case let .groupRcv(groupMember) = chatItem.chatDir { - let m = Text(groupMember.memberProfile.displayName) - return (preview ? m : m.font(.headline)) + Text(": ") + res - } else { - return res - } -} - -private func formattedText(_ ft: FormattedText, _ preview: Bool) -> Text { - let t = ft.text - if let f = ft.format { - switch (f) { - case .bold: return Text(t).bold() - case .italic: return Text(t).italic() - case .strikeThrough: return Text(t).strikethrough() - case .snippet: return Text(t).font(.body.monospaced()) - case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary) - case let .colored(color): return Text(t).foregroundColor(color.uiColor) - case .uri: return linkText(t, t, preview, prefix: "") - case .email: return linkText(t, t, preview, prefix: "mailto:") - case .phone: return linkText(t, t.replacingOccurrences(of: " ", with: ""), preview, prefix: "tel:") - } - } else { - return Text(t) - } -} - -private func linkText(_ s: String, _ link: String, - _ preview: Bool, prefix: String) -> Text { - preview - ? Text(s).foregroundColor(linkColor).underline(color: linkColor) - : Text(AttributedString(s, attributes: AttributeContainer([ - .link: NSURL(string: prefix + link) as Any, - .foregroundColor: uiLinkColor as Any - ]))).underline() -} - -struct TextItemView_Previews: PreviewProvider { - static var previews: some View { - Group{ - TextItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), width: 360) - TextItemView(chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello"), width: 360) - TextItemView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent), width: 360) - TextItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), width: 360) - TextItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), width: 360) - TextItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), width: 360) - TextItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), width: 360) - } - .previewLayout(.fixed(width: 360, height: 70)) - } -} diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index e287e8c1ae..c778a6b08b 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -10,13 +10,12 @@ import SwiftUI struct ChatItemView: View { var chatItem: ChatItem - var width: CGFloat var body: some View { - if (isShortEmoji(chatItem.content.text)) { + if (chatItem.quotedItem == nil && isShortEmoji(chatItem.content.text)) { EmojiItemView(chatItem: chatItem) } else { - TextItemView(chatItem: chatItem, width: width) + FramedItemView(chatItem: chatItem) } } } @@ -24,11 +23,11 @@ struct ChatItemView: View { struct ChatItemView_Previews: PreviewProvider { static var previews: some View { Group{ - ChatItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), width: 360) - ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), width: 360) - ChatItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), width: 360) - ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), width: 360) - ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), width: 360) + ChatItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello")) + ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too")) + ChatItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂")) + ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂")) + ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂")) } .previewLayout(.fixed(width: 360, height: 70)) } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 540a88af62..5860cfdf6a 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -12,6 +12,7 @@ struct ChatView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.colorScheme) var colorScheme @ObservedObject var chat: Chat + @State var quotedItem: ChatItem? = nil @State private var inProgress: Bool = false @FocusState private var keyboardVisible: Bool @State private var showChatInfo = false @@ -21,12 +22,27 @@ struct ChatView: View { return VStack { GeometryReader { g in + let maxWidth = g.size.width * 0.78 ScrollViewReader { proxy in ScrollView { - VStack(spacing: 5) { - ForEach(chatModel.chatItems, id: \.id) { - ChatItemView(chatItem: $0, width: g.size.width) - .frame(minWidth: 0, maxWidth: .infinity, alignment: $0.chatDir.sent ? .trailing : .leading) + LazyVStack(spacing: 5) { + ForEach(chatModel.chatItems) { ci in + let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading + ChatItemView(chatItem: ci) + .contextMenu { + Button { + withAnimation { quotedItem = ci } + } label: { Label("Reply", systemImage: "arrowshape.turn.up.left") } + Button { + showShareSheet(items: [ci.content.text]) + } label: { Label("Share", systemImage: "square.and.arrow.up") } + Button { + UIPasteboard.general.string = ci.content.text + } label: { Label("Copy", systemImage: "doc.on.doc") } + } + .padding(.horizontal) + .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) + .frame(minWidth: 0, maxWidth: .infinity, alignment: alignment) } .onAppear { DispatchQueue.main.async { @@ -54,7 +70,8 @@ struct ChatView: View { Spacer(minLength: 0) - SendMessageView( + ComposeView( + quotedItem: $quotedItem, sendMessage: sendMessage, inProgress: inProgress, keyboardVisible: $keyboardVisible @@ -115,8 +132,14 @@ struct ChatView: View { func sendMessage(_ msg: String) { Task { do { - let chatItem = try await apiSendMessage(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, msg: .text(msg)) + let chatItem = try await apiSendMessage( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + quotedItemId: quotedItem?.meta.itemId, + msg: .text(msg) + ) DispatchQueue.main.async { + quotedItem = nil chatModel.addChatItem(chat.chatInfo, chatItem) } } catch { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift new file mode 100644 index 0000000000..1e02ee3eeb --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -0,0 +1,42 @@ +// +// ComposeView.swift +// SimpleX +// +// Created by Evgeny on 13/03/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ComposeView: View { + @Binding var quotedItem: ChatItem? + var sendMessage: (String) -> Void + var inProgress: Bool = false + @FocusState.Binding var keyboardVisible: Bool + + var body: some View { + VStack(spacing: 0) { + QuotedItemView(quotedItem: $quotedItem) + .transition(.move(edge: .bottom)) + SendMessageView( + sendMessage: sendMessage, + inProgress: inProgress, + keyboardVisible: $keyboardVisible + ) + .background(.background) + } + } +} + +struct ComposeView_Previews: PreviewProvider { + static var previews: some View { + @FocusState var keyboardVisible: Bool + @State var quotedItem: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello") + + return ComposeView( + quotedItem: $quotedItem, + sendMessage: { print ($0) }, + keyboardVisible: $keyboardVisible + ) + } +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/QuotedItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/QuotedItemView.swift new file mode 100644 index 0000000000..36a49637ac --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/QuotedItemView.swift @@ -0,0 +1,49 @@ +// +// QuotedItemView.swift +// SimpleX +// +// Created by Evgeny on 13/03/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct QuotedItemView: View { + @Environment(\.colorScheme) var colorScheme + @Binding var quotedItem: ChatItem? + + var body: some View { + if let qi = quotedItem { + HStack { + quoteText(qi).lineLimit(3) + Spacer() + Button { + withAnimation { quotedItem = nil } + } label: { + Image(systemName: "multiply") + } + } + .padding(12) + .frame(maxWidth: .infinity) + .background(chatItemFrameColor(qi, colorScheme)) + .padding(.top, 8) + } else { + EmptyView() + } + } + + func quoteText(_ qi: ChatItem) -> some View { + if let s = qi.memberDisplayName { + return (Text(s).fontWeight(.medium) + Text(": \(qi.content.text)")) + } else { + return Text(qi.content.text) + } + } +} + +struct QuotedItemView_Previews: PreviewProvider { + static var previews: some View { + @State var quotedItem: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello") + return QuotedItemView(quotedItem: $quotedItem) + } +} diff --git a/apps/ios/Shared/Views/Chat/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift similarity index 100% rename from apps/ios/Shared/Views/Chat/SendMessageView.swift rename to apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 385c909dfd..89694d1529 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -51,7 +51,7 @@ struct ChatPreviewView: View { if let cItem = cItem { ZStack(alignment: .topTrailing) { - (itemStatusMark(cItem) + messageText(cItem, preview: true)) + (itemStatusMark(cItem) + messageText(cItem.content, cItem.formattedText, cItem.memberDisplayName, preview: true)) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading) .padding(.leading, 8) .padding(.trailing, 36) diff --git a/apps/ios/Shared/Views/Helpers/DetermineWidth.swift b/apps/ios/Shared/Views/Helpers/DetermineWidth.swift new file mode 100644 index 0000000000..ba6577a9b0 --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/DetermineWidth.swift @@ -0,0 +1,35 @@ +// +// DetermineWidth.swift +// SimpleX +// +// Created by Evgeny on 14/03/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct DetermineWidth: View { + typealias Key = MaximumWidthPreferenceKey + var body: some View { + GeometryReader { + proxy in + Color.clear + .anchorPreference(key: Key.self, value: .bounds) { + anchor in proxy[anchor].size.width + } + } + } +} + +struct MaximumWidthPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = max(value, nextValue()) + } +} + +struct DetermineWidth_Previews: PreviewProvider { + static var previews: some View { + DetermineWidth() + } +} diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 28f9c46b5e..2734f06105 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -9,6 +9,16 @@ /* Begin PBXBuildFile section */ 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; }; 5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; }; + 5C0E5EF627E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0E5EF127E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj.a */; }; + 5C0E5EF727E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0E5EF127E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj.a */; }; + 5C0E5EF827E24676003DE3D0 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0E5EF227E24676003DE3D0 /* libffi.a */; }; + 5C0E5EF927E24676003DE3D0 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0E5EF227E24676003DE3D0 /* libffi.a */; }; + 5C0E5EFA27E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0E5EF327E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj-ghc8.10.7.a */; }; + 5C0E5EFB27E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0E5EF327E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj-ghc8.10.7.a */; }; + 5C0E5EFC27E24676003DE3D0 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0E5EF427E24676003DE3D0 /* libgmp.a */; }; + 5C0E5EFD27E24676003DE3D0 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0E5EF427E24676003DE3D0 /* libgmp.a */; }; + 5C0E5EFE27E24676003DE3D0 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0E5EF527E24676003DE3D0 /* libgmpxx.a */; }; + 5C0E5EFF27E24676003DE3D0 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0E5EF527E24676003DE3D0 /* libgmpxx.a */; }; 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; 5C116CDD27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; }; @@ -25,6 +35,10 @@ 5C35CFC927B2782E00FB6C6D /* BGManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFC727B2782E00FB6C6D /* BGManager.swift */; }; 5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */; }; 5C35CFCC27B2E91D00FB6C6D /* NtfManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */; }; + 5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */; }; + 5C3A88CF27DF50170060F1C2 /* DetermineWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */; }; + 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */; }; + 5C3A88D227DF57800060F1C2 /* FramedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */; }; 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C5346A927B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; }; @@ -45,16 +59,6 @@ 5C764E85279C748C000C6508 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C764E7C279C71DB000C6508 /* libz.tbd */; }; 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; }; 5C764E8A279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; }; - 5C79C23E27DB673900C829D6 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C79C23927DB673800C829D6 /* libffi.a */; }; - 5C79C23F27DB673900C829D6 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C79C23927DB673800C829D6 /* libffi.a */; }; - 5C79C24027DB673900C829D6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C79C23A27DB673800C829D6 /* libgmp.a */; }; - 5C79C24127DB673900C829D6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C79C23A27DB673800C829D6 /* libgmp.a */; }; - 5C79C24227DB673900C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C79C23B27DB673800C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2.a */; }; - 5C79C24327DB673900C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C79C23B27DB673800C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2.a */; }; - 5C79C24427DB673900C829D6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C79C23C27DB673900C829D6 /* libgmpxx.a */; }; - 5C79C24527DB673900C829D6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C79C23C27DB673900C829D6 /* libgmpxx.a */; }; - 5C79C24627DB673900C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C79C23D27DB673900C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2-ghc8.10.7.a */; }; - 5C79C24727DB673900C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C79C23D27DB673900C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2-ghc8.10.7.a */; }; 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; }; 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */; }; 5C971E1E27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */; }; @@ -98,10 +102,14 @@ 5CCD403B27A5F9BE00368C90 /* CreateGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */; }; 5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407127ADB1D0007B033A /* Emoji.swift */; }; 5CE4407327ADB1D0007B033A /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407127ADB1D0007B033A /* Emoji.swift */; }; - 5CE4407627ADB66A007B033A /* TextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407527ADB66A007B033A /* TextItemView.swift */; }; - 5CE4407727ADB66A007B033A /* TextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407527ADB66A007B033A /* TextItemView.swift */; }; 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; }; 5CE4407A27ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; }; + 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; }; + 5CEACCE427DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; }; + 5CEACCE727DE97B6000BD591 /* QuotedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE627DE97B6000BD591 /* QuotedItemView.swift */; }; + 5CEACCE827DE97B6000BD591 /* QuotedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE627DE97B6000BD591 /* QuotedItemView.swift */; }; + 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; + 5CEACCEE27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; 640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; }; 640F50E427CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; }; /* End PBXBuildFile section */ @@ -125,15 +133,21 @@ /* Begin PBXFileReference section */ 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = ""; }; + 5C0E5EF127E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj.a"; sourceTree = ""; }; + 5C0E5EF227E24676003DE3D0 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C0E5EF327E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj-ghc8.10.7.a"; sourceTree = ""; }; + 5C0E5EF427E24676003DE3D0 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C0E5EF527E24676003DE3D0 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = ""; }; 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = ""; }; 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = ""; }; - 5C2E260927A2C63500F70299 /* MyPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = MyPlayground.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = ""; }; 5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = ""; }; 5C2E261127A30FEA00F70299 /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = ""; }; 5C35CFC727B2782E00FB6C6D /* BGManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGManager.swift; sourceTree = ""; }; 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfManager.swift; sourceTree = ""; }; + 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineWidth.swift; sourceTree = ""; }; + 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramedItemView.swift; sourceTree = ""; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = ""; }; 5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = ""; }; 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHelp.swift; sourceTree = ""; }; @@ -147,11 +161,6 @@ 5C764E7E279C7275000C6508 /* SimpleX (macOS)-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SimpleX (macOS)-Bridging-Header.h"; sourceTree = ""; }; 5C764E7F279C7276000C6508 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = ""; }; 5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = ""; }; - 5C79C23927DB673800C829D6 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C79C23A27DB673800C829D6 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C79C23B27DB673800C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2.a"; sourceTree = ""; }; - 5C79C23C27DB673900C829D6 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5C79C23D27DB673900C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2-ghc8.10.7.a"; sourceTree = ""; }; 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = ""; }; 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = ""; }; 5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; @@ -179,8 +188,10 @@ 5CCD403627A5F9A200368C90 /* ConnectContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectContactView.swift; sourceTree = ""; }; 5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupView.swift; sourceTree = ""; }; 5CE4407127ADB1D0007B033A /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; - 5CE4407527ADB66A007B033A /* TextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextItemView.swift; sourceTree = ""; }; 5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = ""; }; + 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; + 5CEACCE627DE97B6000BD591 /* QuotedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedItemView.swift; sourceTree = ""; }; + 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; 640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -190,13 +201,13 @@ buildActionMask = 2147483647; files = ( 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */, - 5C79C24027DB673900C829D6 /* libgmp.a in Frameworks */, + 5C0E5EFC27E24676003DE3D0 /* libgmp.a in Frameworks */, 5C764E83279C748B000C6508 /* libz.tbd in Frameworks */, + 5C0E5EF627E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj.a in Frameworks */, + 5C0E5EFE27E24676003DE3D0 /* libgmpxx.a in Frameworks */, + 5C0E5EF827E24676003DE3D0 /* libffi.a in Frameworks */, 5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */, - 5C79C23E27DB673900C829D6 /* libffi.a in Frameworks */, - 5C79C24227DB673900C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2.a in Frameworks */, - 5C79C24627DB673900C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2-ghc8.10.7.a in Frameworks */, - 5C79C24427DB673900C829D6 /* libgmpxx.a in Frameworks */, + 5C0E5EFA27E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj-ghc8.10.7.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -204,13 +215,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C79C24527DB673900C829D6 /* libgmpxx.a in Frameworks */, - 5C79C24727DB673900C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2-ghc8.10.7.a in Frameworks */, 5C764E85279C748C000C6508 /* libz.tbd in Frameworks */, - 5C79C23F27DB673900C829D6 /* libffi.a in Frameworks */, - 5C79C24127DB673900C829D6 /* libgmp.a in Frameworks */, + 5C0E5EF727E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj.a in Frameworks */, 5C764E84279C748C000C6508 /* libiconv.tbd in Frameworks */, - 5C79C24327DB673900C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2.a in Frameworks */, + 5C0E5EFB27E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj-ghc8.10.7.a in Frameworks */, + 5C0E5EFD27E24676003DE3D0 /* libgmp.a in Frameworks */, + 5C0E5EF927E24676003DE3D0 /* libffi.a in Frameworks */, + 5C0E5EFF27E24676003DE3D0 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -249,10 +260,10 @@ isa = PBXGroup; children = ( 5CE4407427ADB657007B033A /* ChatItem */, + 5CEACCE527DE977C000BD591 /* ComposeMessage */, 5C2E260E27A30FDC00F70299 /* ChatView.swift */, 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */, 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */, - 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */, 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */, 5CE4407127ADB1D0007B033A /* Emoji.swift */, ); @@ -262,11 +273,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C79C23927DB673800C829D6 /* libffi.a */, - 5C79C23A27DB673800C829D6 /* libgmp.a */, - 5C79C23C27DB673900C829D6 /* libgmpxx.a */, - 5C79C23D27DB673900C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2-ghc8.10.7.a */, - 5C79C23B27DB673800C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2.a */, + 5C0E5EF227E24676003DE3D0 /* libffi.a */, + 5C0E5EF427E24676003DE3D0 /* libgmp.a */, + 5C0E5EF527E24676003DE3D0 /* libgmpxx.a */, + 5C0E5EF327E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj-ghc8.10.7.a */, + 5C0E5EF127E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj.a */, ); path = Libraries; sourceTree = ""; @@ -298,6 +309,7 @@ 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */, 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */, 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */, + 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */, ); path = Helpers; sourceTree = ""; @@ -327,7 +339,6 @@ 5C764E7D279C7275000C6508 /* SimpleX (iOS)-Bridging-Header.h */, 5C764E7E279C7275000C6508 /* SimpleX (macOS)-Bridging-Header.h */, 5C764E7F279C7276000C6508 /* dummy.m */, - 5C2E260927A2C63500F70299 /* MyPlayground.playground */, ); path = Shared; sourceTree = ""; @@ -408,13 +419,24 @@ 5CE4407427ADB657007B033A /* ChatItem */ = { isa = PBXGroup; children = ( - 5CE4407527ADB66A007B033A /* TextItemView.swift */, 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */, 5CE4407827ADB701007B033A /* EmojiItemView.swift */, + 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */, + 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */, ); path = ChatItem; sourceTree = ""; }; + 5CEACCE527DE977C000BD591 /* ComposeMessage */ = { + isa = PBXGroup; + children = ( + 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */, + 5CEACCE227DE9246000BD591 /* ComposeView.swift */, + 5CEACCE627DE97B6000BD591 /* QuotedItemView.swift */, + ); + path = ComposeMessage; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -584,14 +606,16 @@ files = ( 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, - 5CE4407627ADB66A007B033A /* TextItemView.swift in Sources */, + 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */, 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */, 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */, 5C764E80279C7276000C6508 /* dummy.m in Sources */, 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, + 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */, 640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */, + 5CEACCE727DE97B6000BD591 /* QuotedItemView.swift in Sources */, 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */, 5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */, 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */, @@ -602,6 +626,7 @@ 5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */, 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */, 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */, + 5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */, 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */, 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */, 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */, @@ -613,6 +638,7 @@ 5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */, 5CCD403727A5F9A200368C90 /* ConnectContactView.swift in Sources */, 5CCD403A27A5F9BE00368C90 /* CreateGroupView.swift in Sources */, + 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */, 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */, 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */, 5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */, @@ -629,14 +655,16 @@ files = ( 5C6AD81427A834E300348BD7 /* NewChatButton.swift in Sources */, 5CB924D827A8563F00ACCCDD /* SettingsView.swift in Sources */, - 5CE4407727ADB66A007B033A /* TextItemView.swift in Sources */, + 5CEACCE427DE9246000BD591 /* ComposeView.swift in Sources */, 5CB924E227A867BA00ACCCDD /* UserProfile.swift in Sources */, 5CE4407A27ADB701007B033A /* EmojiItemView.swift in Sources */, 5C5346A927B59A6A004DF848 /* ChatHelp.swift in Sources */, 5C764E81279C7276000C6508 /* dummy.m in Sources */, 5C7505A927B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, + 5C3A88D227DF57800060F1C2 /* FramedItemView.swift in Sources */, 5CB924E527A8683A00ACCCDD /* UserAddress.swift in Sources */, 640F50E427CF991C001E05C2 /* SMPServers.swift in Sources */, + 5CEACCE827DE97B6000BD591 /* QuotedItemView.swift in Sources */, 5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */, 5C35CFCC27B2E91D00FB6C6D /* NtfManager.swift in Sources */, 5C2E261327A30FEA00F70299 /* TerminalView.swift in Sources */, @@ -647,6 +675,7 @@ 5CB9250E27A9432000ACCCDD /* ChatListNavLink.swift in Sources */, 5CA059EE279559F40002BEB4 /* ContentView.swift in Sources */, 5CCD403527A5F6DF00368C90 /* AddContactView.swift in Sources */, + 5C3A88CF27DF50170060F1C2 /* DetermineWidth.swift in Sources */, 5C7505A627B679EE00BE3227 /* NavLinkPlain.swift in Sources */, 5C7505A327B65FDB00BE3227 /* CIMetaView.swift in Sources */, 5C35CFC927B2782E00FB6C6D /* BGManager.swift in Sources */, @@ -658,6 +687,7 @@ 5CA059EC279559F40002BEB4 /* SimpleXApp.swift in Sources */, 5CCD403827A5F9A200368C90 /* ConnectContactView.swift in Sources */, 5CCD403B27A5F9BE00368C90 /* CreateGroupView.swift in Sources */, + 5CEACCEE27DEA495000BD591 /* MsgContentView.swift in Sources */, 5C764E8A279CBCB3000C6508 /* ChatModel.swift in Sources */, 5C971E1E27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */, 5CC1C99627A6CF7F000D9FF6 /* ShareSheet.swift in Sources */, @@ -839,6 +869,10 @@ "$(inherited)", "@executable_path/Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Libraries", + ); "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios"; "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim"; MARKETING_VERSION = 1.2; @@ -879,6 +913,10 @@ "$(inherited)", "@executable_path/Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Libraries", + ); "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios"; "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim"; MARKETING_VERSION = 1.2; diff --git a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..9d4b1de373 --- /dev/null +++ b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "CodeScanner", + "repositoryURL": "https://github.com/twostraws/CodeScanner", + "state": { + "branch": null, + "revision": "c27a66149b7483fe42e2ec6aad61d5c3fffe522d", + "version": "2.1.1" + } + } + ] + }, + "version": 1 +} diff --git a/cabal.project b/cabal.project index f2fe863ece..4d47cbcee4 100644 --- a/cabal.project +++ b/cabal.project @@ -2,20 +2,20 @@ packages: . source-repository-package type: git - location: git://github.com/simplex-chat/simplexmq.git + location: https://github.com/simplex-chat/simplexmq.git tag: 5c6ec96d6477371d8e617bcc71e6ecbcdd5c78cc source-repository-package type: git - location: git://github.com/simplex-chat/aeson.git + location: https://github.com/simplex-chat/aeson.git tag: 3eb66f9a68f103b5f1489382aad89f5712a64db7 source-repository-package type: git - location: git://github.com/simplex-chat/haskell-terminal.git + location: https://github.com/simplex-chat/haskell-terminal.git tag: f708b00009b54890172068f168bf98508ffcd495 source-repository-package type: git - location: git://github.com/zw3rk/android-support.git + location: https://github.com/zw3rk/android-support.git tag: 3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb