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
This commit is contained in:
parent
148474e1ba
commit
744c451927
@ -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"
|
||||
|
@ -112,9 +112,8 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
//fun testJson() {
|
||||
// val str = """
|
||||
// {}
|
||||
// val str: String = """
|
||||
// """.trimIndent()
|
||||
//
|
||||
// println(json.decodeFromString<ChatItem>(str))
|
||||
// println(json.decodeFromString<APIResponse>(str))
|
||||
//}
|
||||
|
@ -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) {
|
||||
|
@ -466,24 +466,31 @@ data class ChatItem (
|
||||
val chatDir: CIDirection,
|
||||
val meta: CIMeta,
|
||||
val content: CIContent,
|
||||
val formattedText: List<FormattedText>? = null
|
||||
val formattedText: List<FormattedText>? = 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<FormattedText>? = 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
|
||||
|
@ -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<String>): 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)),
|
||||
|
@ -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<ChatItem?>(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<ChatItem>,
|
||||
user: User,
|
||||
chat: Chat,
|
||||
chatItems: List<ChatItem>,
|
||||
quotedItem: MutableState<ChatItem?>,
|
||||
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<ChatItem>) {
|
||||
fun ChatItemsList(user: User, chatItems: List<ChatItem>, quotedItem: MutableState<ChatItem?>) {
|
||||
val listState = rememberLazyListState()
|
||||
val keyboardState by getKeyboardState()
|
||||
val ciListState = rememberSaveable(stateSaver = CIListStateSaver) {
|
||||
@ -160,9 +169,10 @@ fun ChatItemsList(chatItems: List<ChatItem>) {
|
||||
}
|
||||
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 = {}
|
||||
|
@ -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<ChatItem?>, sendMessage: (String) -> Unit) {
|
||||
Column {
|
||||
QuotedItemView(quotedItem)
|
||||
SendMsgView(sendMessage)
|
||||
}
|
||||
}
|
@ -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<ChatItem?>) {
|
||||
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"
|
||||
))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -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 = ""
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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<ChatItem?>, 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -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<FormattedText>? = 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."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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!
|
||||
|
@ -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{
|
||||
|
112
apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
Normal file
112
apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
Normal file
@ -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))
|
||||
}
|
||||
}
|
97
apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
Normal file
97
apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift
Normal file
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
42
apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
Normal file
42
apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
Normal file
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
35
apps/ios/Shared/Views/Helpers/DetermineWidth.swift
Normal file
35
apps/ios/Shared/Views/Helpers/DetermineWidth.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
@ -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 = "<group>"; };
|
||||
5C0E5EF127E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj.a"; sourceTree = "<group>"; };
|
||||
5C0E5EF227E24676003DE3D0 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
5C0E5EF427E24676003DE3D0 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C0E5EF527E24676003DE3D0 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = "<group>"; };
|
||||
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; };
|
||||
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; };
|
||||
5C2E260927A2C63500F70299 /* MyPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = MyPlayground.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; };
|
||||
5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
|
||||
5C2E261127A30FEA00F70299 /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = "<group>"; };
|
||||
5C35CFC727B2782E00FB6C6D /* BGManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGManager.swift; sourceTree = "<group>"; };
|
||||
5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfManager.swift; sourceTree = "<group>"; };
|
||||
5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineWidth.swift; sourceTree = "<group>"; };
|
||||
5C3A88D027DF57800060F1C2 /* FramedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramedItemView.swift; sourceTree = "<group>"; };
|
||||
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
|
||||
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
|
||||
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHelp.swift; sourceTree = "<group>"; };
|
||||
@ -147,11 +161,6 @@
|
||||
5C764E7E279C7275000C6508 /* SimpleX (macOS)-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SimpleX (macOS)-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
5C764E7F279C7276000C6508 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = "<group>"; };
|
||||
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
|
||||
5C79C23927DB673800C829D6 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C79C23A27DB673800C829D6 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C79C23B27DB673800C829D6 /* libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.3.1-9p94HNy0jcDHEpXhSSIQi2.a"; sourceTree = "<group>"; };
|
||||
5C79C23C27DB673900C829D6 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = "<group>"; };
|
||||
5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = "<group>"; };
|
||||
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
|
||||
@ -179,8 +188,10 @@
|
||||
5CCD403627A5F9A200368C90 /* ConnectContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectContactView.swift; sourceTree = "<group>"; };
|
||||
5CCD403927A5F9BE00368C90 /* CreateGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupView.swift; sourceTree = "<group>"; };
|
||||
5CE4407127ADB1D0007B033A /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; };
|
||||
5CE4407527ADB66A007B033A /* TextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextItemView.swift; sourceTree = "<group>"; };
|
||||
5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = "<group>"; };
|
||||
5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
|
||||
5CEACCE627DE97B6000BD591 /* QuotedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedItemView.swift; sourceTree = "<group>"; };
|
||||
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
|
||||
640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
@ -298,6 +309,7 @@
|
||||
5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */,
|
||||
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */,
|
||||
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */,
|
||||
5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
@ -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 = "<group>";
|
||||
@ -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 = "<group>";
|
||||
};
|
||||
5CEACCE527DE977C000BD591 /* ComposeMessage */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */,
|
||||
5CEACCE227DE9246000BD591 /* ComposeView.swift */,
|
||||
5CEACCE627DE97B6000BD591 /* QuotedItemView.swift */,
|
||||
);
|
||||
path = ComposeMessage;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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;
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user