Merge pull request #476 from simplex-chat/master (v1.4.0 terminal app)
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "chat.simplex.app"
|
||||
minSdk 29
|
||||
targetSdk 32
|
||||
versionCode 16
|
||||
versionName "1.3"
|
||||
versionCode 17
|
||||
versionName "1.4"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@@ -33,6 +34,15 @@
|
||||
<data android:scheme="simplex" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="chat.simplex.app.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -102,6 +102,37 @@ class ChatModel(val controller: ChatController) {
|
||||
}
|
||||
}
|
||||
|
||||
fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean {
|
||||
// update previews
|
||||
val i = getChatIndex(cInfo.id)
|
||||
val chat: Chat
|
||||
val res: Boolean
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.last()
|
||||
if (pItem.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
|
||||
}
|
||||
res = false
|
||||
} else {
|
||||
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
res = true
|
||||
}
|
||||
// update current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
chatItems[itemIndex] = cItem
|
||||
return false
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatItemsRead(cInfo: ChatInfo) {
|
||||
val chatIdx = getChatIndex(cInfo.id)
|
||||
// update current chat
|
||||
@@ -122,42 +153,13 @@ class ChatModel(val controller: ChatController) {
|
||||
}
|
||||
|
||||
}
|
||||
//
|
||||
// func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||
// // update previews
|
||||
// var res: Bool
|
||||
// if let chat = getChat(cInfo.id) {
|
||||
// if let pItem = chat.chatItems.last, pItem.id == cItem.id {
|
||||
// chat.chatItems = [cItem]
|
||||
// }
|
||||
// res = false
|
||||
// } else {
|
||||
// addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
|
||||
// res = true
|
||||
// }
|
||||
// // update current chat
|
||||
// if chatId == cInfo.id {
|
||||
// if let i = chatItems.firstIndex(where: { $0.id == cItem.id }) {
|
||||
// withAnimation(.default) {
|
||||
// self.chatItems[i] = cItem
|
||||
// }
|
||||
// return false
|
||||
// } else {
|
||||
// withAnimation { chatItems.append(cItem) }
|
||||
// return true
|
||||
// }
|
||||
// } else {
|
||||
// return res
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
|
||||
// func popChat(_ id: String) {
|
||||
// if let i = getChatIndex(id) {
|
||||
// popChat_(i)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
|
||||
private fun popChat_(i: Int) {
|
||||
val chat = chats.removeAt(i)
|
||||
chats.add(index = 0, chat)
|
||||
@@ -191,6 +193,7 @@ data class User(
|
||||
): NamedChat {
|
||||
override val displayName: String get() = profile.displayName
|
||||
override val fullName: String get() = profile.fullName
|
||||
override val image: String? get() = profile.image
|
||||
|
||||
companion object {
|
||||
val sampleData = User(
|
||||
@@ -208,6 +211,7 @@ typealias ChatId = String
|
||||
interface NamedChat {
|
||||
val displayName: String
|
||||
val fullName: String
|
||||
val image: String?
|
||||
val chatViewName: String
|
||||
get() = displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName")
|
||||
}
|
||||
@@ -272,6 +276,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val createdAt get() = contact.createdAt
|
||||
override val displayName get() = contact.displayName
|
||||
override val fullName get() = contact.fullName
|
||||
override val image get() = contact.image
|
||||
|
||||
companion object {
|
||||
val sampleData = Direct(Contact.sampleData)
|
||||
@@ -288,6 +293,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val createdAt get() = groupInfo.createdAt
|
||||
override val displayName get() = groupInfo.displayName
|
||||
override val fullName get() = groupInfo.fullName
|
||||
override val image get() = groupInfo.image
|
||||
|
||||
companion object {
|
||||
val sampleData = Group(GroupInfo.sampleData)
|
||||
@@ -304,6 +310,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val createdAt get() = contactRequest.createdAt
|
||||
override val displayName get() = contactRequest.displayName
|
||||
override val fullName get() = contactRequest.fullName
|
||||
override val image get() = contactRequest.image
|
||||
|
||||
companion object {
|
||||
val sampleData = ContactRequest(UserContactRequest.sampleData)
|
||||
@@ -326,6 +333,7 @@ class Contact(
|
||||
override val ready get() = activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready"
|
||||
override val displayName get() = profile.displayName
|
||||
override val fullName get() = profile.fullName
|
||||
override val image get() = profile.image
|
||||
|
||||
companion object {
|
||||
val sampleData = Contact(
|
||||
@@ -354,7 +362,8 @@ class Connection(val connStatus: String) {
|
||||
@Serializable
|
||||
class Profile(
|
||||
val displayName: String,
|
||||
val fullName: String
|
||||
val fullName: String,
|
||||
val image: String? = null
|
||||
) {
|
||||
companion object {
|
||||
val sampleData = Profile(
|
||||
@@ -377,6 +386,7 @@ class GroupInfo (
|
||||
override val ready get() = true
|
||||
override val displayName get() = groupProfile.displayName
|
||||
override val fullName get() = groupProfile.fullName
|
||||
override val image get() = groupProfile.image
|
||||
|
||||
companion object {
|
||||
val sampleData = GroupInfo(
|
||||
@@ -391,7 +401,8 @@ class GroupInfo (
|
||||
@Serializable
|
||||
class GroupProfile (
|
||||
override val displayName: String,
|
||||
override val fullName: String
|
||||
override val fullName: String,
|
||||
override val image: String? = null
|
||||
): NamedChat {
|
||||
companion object {
|
||||
val sampleData = GroupProfile(
|
||||
@@ -444,6 +455,7 @@ class UserContactRequest (
|
||||
override val ready get() = true
|
||||
override val displayName get() = profile.displayName
|
||||
override val fullName get() = profile.fullName
|
||||
override val image get() = profile.image
|
||||
|
||||
companion object {
|
||||
val sampleData = UserContactRequest(
|
||||
@@ -484,11 +496,14 @@ data class ChatItem (
|
||||
ts: Instant = Clock.System.now(),
|
||||
text: String = "hello\nthere",
|
||||
status: CIStatus = CIStatus.SndNew(),
|
||||
quotedItem: CIQuote? = null
|
||||
quotedItem: CIQuote? = null,
|
||||
itemDeleted: Boolean = false,
|
||||
itemEdited: Boolean = false,
|
||||
editable: Boolean = true
|
||||
) =
|
||||
ChatItem(
|
||||
chatDir = dir,
|
||||
meta = CIMeta.getSample(id, ts, text, status),
|
||||
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable),
|
||||
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
|
||||
quotedItem = quotedItem
|
||||
)
|
||||
@@ -526,18 +541,27 @@ data class CIMeta (
|
||||
val itemTs: Instant,
|
||||
val itemText: String,
|
||||
val itemStatus: CIStatus,
|
||||
val createdAt: Instant
|
||||
val createdAt: Instant,
|
||||
val itemDeleted: Boolean,
|
||||
val itemEdited: Boolean,
|
||||
val editable: Boolean
|
||||
) {
|
||||
val timestampText: String get() = getTimestampText(itemTs)
|
||||
|
||||
companion object {
|
||||
fun getSample(id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew()): CIMeta =
|
||||
fun getSample(
|
||||
id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(),
|
||||
itemDeleted: Boolean = false, itemEdited: Boolean = false, editable: Boolean = true
|
||||
): CIMeta =
|
||||
CIMeta(
|
||||
itemId = id,
|
||||
itemTs = ts,
|
||||
itemText = text,
|
||||
itemStatus = status,
|
||||
createdAt = ts
|
||||
createdAt = ts,
|
||||
itemDeleted = itemDeleted,
|
||||
itemEdited = itemEdited,
|
||||
editable = editable
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -641,6 +665,7 @@ sealed class MsgContent {
|
||||
}
|
||||
|
||||
object MsgContentSerializer : KSerializer<MsgContent> {
|
||||
@OptIn(InternalSerializationApi::class)
|
||||
override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) {
|
||||
element("MCText", buildClassSerialDescriptor("MCText") {
|
||||
element<String>("text")
|
||||
|
||||
@@ -131,6 +131,20 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiUpdateMessage(type: ChatType, id: Long, itemId: Long, mc: MsgContent): AChatItem? {
|
||||
val r = sendCmd(CC.ApiUpdateMessage(type, id, itemId, mc))
|
||||
if (r is CR.ChatItemUpdated) return r.chatItem
|
||||
Log.e(TAG, "apiUpdateMessage bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiDeleteMessage(type: ChatType, id: Long, itemId: Long, mode: MsgDeleteMode): AChatItem? {
|
||||
val r = sendCmd(CC.ApiDeleteMessage(type, id, itemId, mode))
|
||||
if (r is CR.ChatItemDeleted) return r.chatItem
|
||||
Log.e(TAG, "apiDeleteMessage bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun getUserSMPServers(): List<String>? {
|
||||
val r = sendCmd(CC.GetUserSMPServers())
|
||||
if (r is CR.UserSMPServers) return r.smpServers
|
||||
@@ -203,7 +217,7 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
}
|
||||
|
||||
suspend fun apiUpdateProfile(profile: Profile): Profile? {
|
||||
val r = sendCmd(CC.UpdateProfile(profile))
|
||||
val r = sendCmd(CC.ApiUpdateProfile(profile))
|
||||
if (r is CR.UserProfileNoChange) return profile
|
||||
if (r is CR.UserProfileUpdated) return r.toProfile
|
||||
Log.e(TAG, "apiUpdateProfile bad response: ${r.responseType} ${r.details}")
|
||||
@@ -303,12 +317,23 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
ntfManager.notifyMessageReceived(cInfo, cItem)
|
||||
}
|
||||
}
|
||||
// case let .chatItemUpdated(aChatItem):
|
||||
// let cInfo = aChatItem.chatInfo
|
||||
// let cItem = aChatItem.chatItem
|
||||
// if chatModel.upsertChatItem(cInfo, cItem) {
|
||||
// NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
// }
|
||||
is CR.ChatItemStatusUpdated -> {
|
||||
val cInfo = r.chatItem.chatInfo
|
||||
val cItem = r.chatItem.chatItem
|
||||
if (chatModel.upsertChatItem(cInfo, cItem)) {
|
||||
ntfManager.notifyMessageReceived(cInfo, cItem)
|
||||
}
|
||||
}
|
||||
is CR.ChatItemUpdated -> {
|
||||
val cInfo = r.chatItem.chatInfo
|
||||
val cItem = r.chatItem.chatItem
|
||||
if (chatModel.upsertChatItem(cInfo, cItem)) {
|
||||
ntfManager.notifyMessageReceived(cInfo, cItem)
|
||||
}
|
||||
}
|
||||
is CR.ChatItemDeleted -> {
|
||||
// TODO
|
||||
}
|
||||
else ->
|
||||
Log.d(TAG , "unsupported event: ${r.responseType}")
|
||||
}
|
||||
@@ -336,6 +361,11 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap
|
||||
}
|
||||
}
|
||||
|
||||
enum class MsgDeleteMode(val mode: String) {
|
||||
Broadcast("broadcast"),
|
||||
Internal("internal");
|
||||
}
|
||||
|
||||
// ChatCommand
|
||||
sealed class CC {
|
||||
class Console(val cmd: String): CC()
|
||||
@@ -346,12 +376,14 @@ sealed class 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 ApiUpdateMessage(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent): CC()
|
||||
class ApiDeleteMessage(val type: ChatType, val id: Long, val itemId: Long, val mode: MsgDeleteMode): CC()
|
||||
class GetUserSMPServers(): CC()
|
||||
class SetUserSMPServers(val smpServers: List<String>): CC()
|
||||
class AddContact: CC()
|
||||
class Connect(val connReq: String): CC()
|
||||
class ApiDeleteChat(val type: ChatType, val id: Long): CC()
|
||||
class UpdateProfile(val profile: Profile): CC()
|
||||
class ApiUpdateProfile(val profile: Profile): CC()
|
||||
class CreateMyAddress: CC()
|
||||
class DeleteMyAddress: CC()
|
||||
class ShowMyAddress: CC()
|
||||
@@ -368,12 +400,14 @@ sealed class CC {
|
||||
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 ApiUpdateMessage -> "/_update item ${chatRef(type, id)} $itemId ${mc.cmdString}"
|
||||
is ApiDeleteMessage -> "/_delete item ${chatRef(type, id)} $itemId $mode"
|
||||
is GetUserSMPServers -> "/smp_servers"
|
||||
is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}"
|
||||
is AddContact -> "/connect"
|
||||
is Connect -> "/connect $connReq"
|
||||
is ApiDeleteChat -> "/_delete ${chatRef(type, id)}"
|
||||
is UpdateProfile -> "/profile ${profile.displayName} ${profile.fullName}"
|
||||
is ApiUpdateProfile -> "/_profile ${json.encodeToString(profile)}"
|
||||
is CreateMyAddress -> "/address"
|
||||
is DeleteMyAddress -> "/delete_address"
|
||||
is ShowMyAddress -> "/show_address"
|
||||
@@ -391,12 +425,14 @@ sealed class CC {
|
||||
is ApiGetChat -> "apiGetChat"
|
||||
is ApiSendMessage -> "apiSendMessage"
|
||||
is ApiSendMessageQuote -> "apiSendMessageQuote"
|
||||
is ApiUpdateMessage -> "apiUpdateMessage"
|
||||
is ApiDeleteMessage -> "apiDeleteMessage"
|
||||
is GetUserSMPServers -> "getUserSMPServers"
|
||||
is SetUserSMPServers -> "setUserSMPServers"
|
||||
is AddContact -> "addContact"
|
||||
is Connect -> "connect"
|
||||
is ApiDeleteChat -> "apiDeleteChat"
|
||||
is UpdateProfile -> "updateProfile"
|
||||
is ApiUpdateProfile -> "updateProfile"
|
||||
is CreateMyAddress -> "createMyAddress"
|
||||
is DeleteMyAddress -> "deleteMyAddress"
|
||||
is ShowMyAddress -> "showMyAddress"
|
||||
@@ -474,7 +510,9 @@ sealed class CR {
|
||||
@Serializable @SerialName("groupEmpty") class GroupEmpty(val group: GroupInfo): CR()
|
||||
@Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR()
|
||||
@Serializable @SerialName("newChatItem") class NewChatItem(val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("cmdOk") class CmdOk: CR()
|
||||
@Serializable @SerialName("chatCmdError") class ChatCmdError(val chatError: ChatError): CR()
|
||||
@Serializable @SerialName("chatError") class ChatRespError(val chatError: ChatError): CR()
|
||||
@@ -512,7 +550,9 @@ sealed class CR {
|
||||
is GroupEmpty -> "groupEmpty"
|
||||
is UserContactLinkSubscribed -> "userContactLinkSubscribed"
|
||||
is NewChatItem -> "newChatItem"
|
||||
is ChatItemStatusUpdated -> "chatItemStatusUpdated"
|
||||
is ChatItemUpdated -> "chatItemUpdated"
|
||||
is ChatItemDeleted -> "chatItemDeleted"
|
||||
is CmdOk -> "cmdOk"
|
||||
is ChatCmdError -> "chatCmdError"
|
||||
is ChatRespError -> "chatError"
|
||||
@@ -551,7 +591,9 @@ sealed class CR {
|
||||
is GroupEmpty -> json.encodeToString(group)
|
||||
is UserContactLinkSubscribed -> noDetails()
|
||||
is NewChatItem -> json.encodeToString(chatItem)
|
||||
is ChatItemStatusUpdated -> json.encodeToString(chatItem)
|
||||
is ChatItemUpdated -> json.encodeToString(chatItem)
|
||||
is ChatItemDeleted -> json.encodeToString(chatItem)
|
||||
is CmdOk -> noDetails()
|
||||
is ChatCmdError -> chatError.string
|
||||
is ChatRespError -> chatError.string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package chat.simplex.app.views
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
@@ -8,8 +9,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
@@ -40,11 +40,11 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TerminalLayout(terminalItems: List<TerminalItem> , close: () -> Unit, sendCommand: (String) -> Unit) {
|
||||
fun TerminalLayout(terminalItems: List<TerminalItem>, close: () -> Unit, sendCommand: (String) -> Unit) {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Scaffold(
|
||||
topBar = { CloseSheetBar(close) },
|
||||
bottomBar = { SendMsgView(sendCommand) },
|
||||
bottomBar = { SendMsgView(msg = remember { mutableStateOf("") }, sendCommand) },
|
||||
modifier = Modifier.navigationBarsWithImePadding()
|
||||
) { contentPadding ->
|
||||
Surface(
|
||||
|
||||
@@ -148,7 +148,7 @@ fun CreateProfilePanel(chatModel: ChatModel) {
|
||||
Button(onClick = {
|
||||
withApi {
|
||||
val user = chatModel.controller.apiCreateActiveUser(
|
||||
Profile(displayName, fullName)
|
||||
Profile(displayName, fullName, null)
|
||||
)
|
||||
chatModel.controller.startChat(user)
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@ package chat.simplex.app.views.chat
|
||||
import android.content.res.Configuration
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.*
|
||||
import androidx.compose.material.icons.outlined.ArrowBack
|
||||
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
|
||||
@@ -23,7 +23,6 @@ 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.*
|
||||
@@ -42,6 +41,8 @@ fun ChatView(chatModel: ChatModel) {
|
||||
chatModel.chatId.value = null
|
||||
} else {
|
||||
val quotedItem = remember { mutableStateOf<ChatItem?>(null) }
|
||||
val editingItem = remember { mutableStateOf<ChatItem?>(null) }
|
||||
var msg = remember { mutableStateOf("") }
|
||||
BackHandler { chatModel.chatId.value = null }
|
||||
// TODO a more advanced version would mark as read only if in view
|
||||
LaunchedEffect(chat.chatItems) {
|
||||
@@ -58,24 +59,37 @@ fun ChatView(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
}
|
||||
ChatLayout(user, chat, chatModel.chatItems, quotedItem,
|
||||
ChatLayout(user, chat, chatModel.chatItems, msg, quotedItem, editingItem,
|
||||
back = { chatModel.chatId.value = null },
|
||||
info = { ModalManager.shared.showCustomModal { close -> ChatInfoView(chatModel, close) } },
|
||||
sendMessage = { msg ->
|
||||
withApi {
|
||||
// show "in progress"
|
||||
val cInfo = chat.chatInfo
|
||||
val newItem = chatModel.controller.apiSendMessage(
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
quotedItemId = quotedItem.value?.meta?.itemId,
|
||||
mc = MsgContent.MCText(msg)
|
||||
)
|
||||
quotedItem.value = null
|
||||
val ei = editingItem.value
|
||||
if (ei != null) {
|
||||
val updatedItem = chatModel.controller.apiUpdateMessage(
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = ei.meta.itemId,
|
||||
mc = MsgContent.MCText(msg)
|
||||
)
|
||||
if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem)
|
||||
} else {
|
||||
val newItem = chatModel.controller.apiSendMessage(
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
quotedItemId = quotedItem.value?.meta?.itemId,
|
||||
mc = MsgContent.MCText(msg)
|
||||
)
|
||||
if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem)
|
||||
}
|
||||
// hide "in progress"
|
||||
if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem)
|
||||
editingItem.value = null
|
||||
quotedItem.value = null
|
||||
}
|
||||
}
|
||||
},
|
||||
resetMessage = { msg.value = "" }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -85,23 +99,27 @@ fun ChatLayout(
|
||||
user: User,
|
||||
chat: Chat,
|
||||
chatItems: List<ChatItem>,
|
||||
msg: MutableState<String>,
|
||||
quotedItem: MutableState<ChatItem?>,
|
||||
editingItem: MutableState<ChatItem?>,
|
||||
back: () -> Unit,
|
||||
info: () -> Unit,
|
||||
sendMessage: (String) -> Unit
|
||||
sendMessage: (String) -> Unit,
|
||||
resetMessage: () -> Unit
|
||||
) {
|
||||
Surface(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.background)) {
|
||||
.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
Scaffold(
|
||||
topBar = { ChatInfoToolbar(chat, back, info) },
|
||||
bottomBar = { ComposeView(quotedItem, sendMessage) },
|
||||
bottomBar = { ComposeView(msg, quotedItem, editingItem, sendMessage, resetMessage) },
|
||||
modifier = Modifier.navigationBarsWithImePadding()
|
||||
) { contentPadding ->
|
||||
Box(Modifier.padding(contentPadding)) {
|
||||
ChatItemsList(user, chatItems, quotedItem)
|
||||
ChatItemsList(user, chatItems, msg, quotedItem, editingItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,14 +152,19 @@ fun ChatInfoToolbar(chat: Chat, back: () -> Unit, info: () -> Unit) {
|
||||
) {
|
||||
val cInfo = chat.chatInfo
|
||||
ChatInfoImage(chat, size = 40.dp)
|
||||
Column(Modifier.padding(start = 8.dp),
|
||||
Column(
|
||||
Modifier.padding(start = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(cInfo.displayName, fontWeight = FontWeight.Bold,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(
|
||||
cInfo.displayName, fontWeight = FontWeight.Bold,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) {
|
||||
Text(cInfo.fullName,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(
|
||||
cInfo.fullName,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,7 +184,13 @@ val CIListStateSaver = run {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatItemsList(user: User, chatItems: List<ChatItem>, quotedItem: MutableState<ChatItem?>) {
|
||||
fun ChatItemsList(
|
||||
user: User,
|
||||
chatItems: List<ChatItem>,
|
||||
msg: MutableState<String>,
|
||||
quotedItem: MutableState<ChatItem?>,
|
||||
editingItem: MutableState<ChatItem?>
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val keyboardState by getKeyboardState()
|
||||
val ciListState = rememberSaveable(stateSaver = CIListStateSaver) {
|
||||
@@ -172,7 +201,7 @@ fun ChatItemsList(user: User, chatItems: List<ChatItem>, quotedItem: MutableStat
|
||||
val cxt = LocalContext.current
|
||||
LazyColumn(state = listState) {
|
||||
items(chatItems) { cItem ->
|
||||
ChatItemView(user, cItem, quotedItem, cxt, uriHandler)
|
||||
ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler)
|
||||
}
|
||||
val len = chatItems.count()
|
||||
if (len > 1 && (keyboardState != ciListState.value.keyboardState || !ciListState.value.scrolled || len != ciListState.value.itemCount)) {
|
||||
@@ -218,10 +247,13 @@ fun PreviewChatLayout() {
|
||||
chatStats = Chat.ChatStats()
|
||||
),
|
||||
chatItems = chatItems,
|
||||
msg = remember { mutableStateOf("") },
|
||||
quotedItem = remember { mutableStateOf(null) },
|
||||
editingItem = remember { mutableStateOf(null) },
|
||||
back = {},
|
||||
info = {},
|
||||
sendMessage = {}
|
||||
sendMessage = {},
|
||||
resetMessage = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
package chat.simplex.app.views.chat
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.*
|
||||
import chat.simplex.app.model.ChatItem
|
||||
|
||||
// TODO ComposeState
|
||||
|
||||
@Composable
|
||||
fun ComposeView(quotedItem: MutableState<ChatItem?>, sendMessage: (String) -> Unit) {
|
||||
fun ComposeView(
|
||||
msg: MutableState<String>,
|
||||
quotedItem: MutableState<ChatItem?>,
|
||||
editingItem: MutableState<ChatItem?>,
|
||||
sendMessage: (String) -> Unit,
|
||||
resetMessage: () -> Unit
|
||||
) {
|
||||
Column {
|
||||
QuotedItemView(quotedItem)
|
||||
SendMsgView(sendMessage)
|
||||
when {
|
||||
quotedItem.value != null -> {
|
||||
ContextItemView(quotedItem)
|
||||
}
|
||||
editingItem.value != null -> {
|
||||
ContextItemView(editingItem, editing = editingItem.value != null, resetMessage)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
SendMsgView(msg, sendMessage, editing = editingItem.value != null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,27 +19,38 @@ 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
|
||||
fun ContextItemView(
|
||||
contextItem: MutableState<ChatItem?>,
|
||||
editing: Boolean = false,
|
||||
resetMessage: () -> Unit = {}
|
||||
) {
|
||||
val cxtItem = contextItem.value
|
||||
if (cxtItem != null) {
|
||||
val sent = cxtItem.chatDir.sent
|
||||
Row(
|
||||
Modifier.padding(top = 8.dp)
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
.background(if (sent) SentColorLight else ReceivedColorLight),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
Modifier.padding(start = 16.dp)
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
.padding(vertical = 12.dp)
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
) {
|
||||
QuoteText(qi)
|
||||
ContextItemText(cxtItem)
|
||||
}
|
||||
IconButton(onClick = { quotedItem.value = null }) {
|
||||
IconButton(onClick = {
|
||||
contextItem.value = null
|
||||
if (editing) {
|
||||
resetMessage()
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Outlined.Close,
|
||||
"Remove quote",
|
||||
contentDescription = "Cancel",
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
@@ -49,14 +60,14 @@ fun QuotedItemView(quotedItem: MutableState<ChatItem?>) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QuoteText(qi: ChatItem) {
|
||||
val member = qi.memberDisplayName
|
||||
private fun ContextItemText(cxtItem: ChatItem) {
|
||||
val member = cxtItem.memberDisplayName
|
||||
if (member == null) {
|
||||
Text(qi.content.text, maxLines = 3)
|
||||
Text(cxtItem.content.text, maxLines = 3)
|
||||
} else {
|
||||
val annotatedText = buildAnnotatedString {
|
||||
withStyle(boldFont) { append(member) }
|
||||
append(": ${qi.content.text}")
|
||||
append(": ${cxtItem.content.text}")
|
||||
}
|
||||
Text(annotatedText, maxLines = 3)
|
||||
}
|
||||
@@ -64,13 +75,15 @@ private fun QuoteText(qi: ChatItem) {
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTextItemViewEmoji() {
|
||||
fun PreviewContextItemView() {
|
||||
SimpleXTheme {
|
||||
QuotedItemView(
|
||||
quotedItem = remember {
|
||||
mutableStateOf(ChatItem.getSampleData(
|
||||
1, CIDirection.DirectRcv(), Clock.System.now(), "hello"
|
||||
))
|
||||
ContextItemView(
|
||||
contextItem = remember {
|
||||
mutableStateOf(
|
||||
ChatItem.getSampleData(
|
||||
1, CIDirection.DirectRcv(), Clock.System.now(), "hello"
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.outlined.ArrowUpward
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -24,15 +25,14 @@ import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.chat.item.*
|
||||
|
||||
@Composable
|
||||
fun SendMsgView(sendMessage: (String) -> Unit) {
|
||||
var msg by remember { mutableStateOf("") }
|
||||
fun SendMsgView(msg: MutableState<String>, sendMessage: (String) -> Unit, editing: Boolean = false) {
|
||||
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
|
||||
var textStyle by remember { mutableStateOf(smallFont) }
|
||||
BasicTextField(
|
||||
value = msg,
|
||||
value = msg.value,
|
||||
onValueChange = {
|
||||
msg = it
|
||||
textStyle = if(isShortEmoji(it)) {
|
||||
msg.value = it
|
||||
textStyle = if (isShortEmoji(it)) {
|
||||
if (it.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont
|
||||
} else {
|
||||
smallFont
|
||||
@@ -64,9 +64,9 @@ fun SendMsgView(sendMessage: (String) -> Unit) {
|
||||
) {
|
||||
innerTextField()
|
||||
}
|
||||
val color = if (msg.isNotEmpty()) MaterialTheme.colors.primary else Color.Gray
|
||||
val color = if (msg.value.isNotEmpty()) MaterialTheme.colors.primary else Color.Gray
|
||||
Icon(
|
||||
Icons.Outlined.ArrowUpward,
|
||||
if (editing) Icons.Filled.Check else Icons.Outlined.ArrowUpward,
|
||||
"Send Message",
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
@@ -75,9 +75,9 @@ fun SendMsgView(sendMessage: (String) -> Unit) {
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.clickable {
|
||||
if (msg.isNotEmpty()) {
|
||||
sendMessage(msg)
|
||||
msg = ""
|
||||
if (msg.value.isNotEmpty()) {
|
||||
sendMessage(msg.value)
|
||||
msg.value = ""
|
||||
textStyle = smallFont
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,25 @@ fun SendMsgView(sendMessage: (String) -> Unit) {
|
||||
fun PreviewSendMsgView() {
|
||||
SimpleXTheme {
|
||||
SendMsgView(
|
||||
msg = remember { mutableStateOf("") },
|
||||
sendMessage = { msg -> println(msg) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewSendMsgViewEditing() {
|
||||
SimpleXTheme {
|
||||
SendMsgView(
|
||||
msg = remember { mutableStateOf("") },
|
||||
sendMessage = { msg -> println(msg) },
|
||||
editing = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
@@ -11,11 +18,24 @@ import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun CIMetaView(chatItem: ChatItem) {
|
||||
Text(
|
||||
chatItem.timestampText,
|
||||
color = HighOrLowlight,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (chatItem.meta.itemEdited) {
|
||||
Icon(
|
||||
Icons.Filled.Edit,
|
||||
modifier = Modifier.height(12.dp),
|
||||
contentDescription = "Edited",
|
||||
tint = HighOrLowlight,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
chatItem.timestampText,
|
||||
color = HighOrLowlight,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@@ -27,3 +47,14 @@ fun PreviewCIMetaView() {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewCIMetaViewEdited() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
itemEdited = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -22,7 +23,15 @@ import chat.simplex.app.views.helpers.shareText
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun ChatItemView(user: User, cItem: ChatItem, quotedItem: MutableState<ChatItem?>, cxt: Context, uriHandler: UriHandler? = null) {
|
||||
fun ChatItemView(
|
||||
user: User,
|
||||
cItem: ChatItem,
|
||||
msg: MutableState<String>,
|
||||
quotedItem: MutableState<ChatItem?>,
|
||||
editingItem: 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) }
|
||||
@@ -44,6 +53,7 @@ fun ChatItemView(user: User, cItem: ChatItem, quotedItem: MutableState<ChatItem?
|
||||
}
|
||||
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
|
||||
ItemAction("Reply", Icons.Outlined.Reply, onClick = {
|
||||
editingItem.value = null
|
||||
quotedItem.value = cItem
|
||||
showMenu = false
|
||||
})
|
||||
@@ -51,10 +61,18 @@ fun ChatItemView(user: User, cItem: ChatItem, quotedItem: MutableState<ChatItem?
|
||||
shareText(cxt, cItem.content.text)
|
||||
showMenu = false
|
||||
})
|
||||
ItemAction("Copy", Icons.Outlined.ContentCopy, onClick = {
|
||||
ItemAction("Copy", Icons.Outlined.ContentCopy, onClick = {
|
||||
copyText(cxt, cItem.content.text)
|
||||
showMenu = false
|
||||
})
|
||||
// if (cItem.chatDir.sent && cItem.meta.editable) {
|
||||
// ItemAction("Edit", Icons.Filled.Edit, onClick = {
|
||||
// quotedItem.value = null
|
||||
// editingItem.value = cItem
|
||||
// msg.value = cItem.content.text
|
||||
// showMenu = false
|
||||
// })
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,9 +82,11 @@ fun ChatItemView(user: User, cItem: ChatItem, quotedItem: MutableState<ChatItem?
|
||||
private fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit) {
|
||||
DropdownMenuItem(onClick) {
|
||||
Row {
|
||||
Text(text, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1F))
|
||||
Text(
|
||||
text, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
)
|
||||
Icon(icon, text, tint = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
@@ -81,7 +101,9 @@ fun PreviewChatItemView() {
|
||||
ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
|
||||
),
|
||||
msg = remember { mutableStateOf("") },
|
||||
quotedItem = remember { mutableStateOf(null) },
|
||||
editingItem = remember { mutableStateOf(null) },
|
||||
cxt = LocalContext.current
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ 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.tooling.preview.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.model.*
|
||||
@@ -48,7 +48,9 @@ fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null) {
|
||||
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
|
||||
if (ci.formattedText == null && isShortEmoji(ci.content.text)) {
|
||||
Column(
|
||||
Modifier.padding(bottom = 2.dp).fillMaxWidth(),
|
||||
Modifier
|
||||
.padding(bottom = 2.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
EmojiText(ci.content.text)
|
||||
@@ -57,7 +59,7 @@ fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null) {
|
||||
} else {
|
||||
MarkdownText(
|
||||
ci.content, ci.formattedText, ci.memberDisplayName,
|
||||
metaText = ci.timestampText, uriHandler = uriHandler, senderBold = true
|
||||
metaText = ci.timestampText, edited = ci.meta.itemEdited, uriHandler = uriHandler, senderBold = true
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -69,14 +71,18 @@ fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null) {
|
||||
}
|
||||
}
|
||||
|
||||
class EditedProvider: PreviewParameterProvider<Boolean> {
|
||||
override val values = listOf(false, true).asSequence()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTextItemViewSnd() {
|
||||
fun PreviewTextItemViewSnd(@PreviewParameter(EditedProvider::class) edited: Boolean) {
|
||||
SimpleXTheme {
|
||||
FramedItemView(
|
||||
User.sampleData,
|
||||
ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", itemEdited = edited
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -84,12 +90,12 @@ fun PreviewTextItemViewSnd() {
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTextItemViewRcv() {
|
||||
fun PreviewTextItemViewRcv(@PreviewParameter(EditedProvider::class) edited: Boolean) {
|
||||
SimpleXTheme {
|
||||
FramedItemView(
|
||||
User.sampleData,
|
||||
ChatItem.getSampleData(
|
||||
1, CIDirection.DirectRcv(), Clock.System.now(), "hello"
|
||||
1, CIDirection.DirectRcv(), Clock.System.now(), "hello", itemEdited = edited
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -97,7 +103,7 @@ fun PreviewTextItemViewRcv() {
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTextItemViewLong() {
|
||||
fun PreviewTextItemViewLong(@PreviewParameter(EditedProvider::class) edited: Boolean) {
|
||||
SimpleXTheme {
|
||||
FramedItemView(
|
||||
User.sampleData,
|
||||
@@ -105,7 +111,8 @@ fun PreviewTextItemViewLong() {
|
||||
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."
|
||||
"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.",
|
||||
itemEdited = edited
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -113,7 +120,7 @@ fun PreviewTextItemViewLong() {
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTextItemViewQuote() {
|
||||
fun PreviewTextItemViewQuote(@PreviewParameter(EditedProvider::class) edited: Boolean) {
|
||||
SimpleXTheme {
|
||||
FramedItemView(
|
||||
User.sampleData,
|
||||
@@ -122,7 +129,8 @@ fun PreviewTextItemViewQuote() {
|
||||
Clock.System.now(),
|
||||
"https://simplex.chat",
|
||||
CIStatus.SndSent(),
|
||||
quotedItem = CIQuote.getSample(1, Clock.System.now(), "hi", chatDir = CIDirection.DirectRcv())
|
||||
quotedItem = CIQuote.getSample(1, Clock.System.now(), "hi", chatDir = CIDirection.DirectRcv()),
|
||||
itemEdited = edited
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -130,7 +138,7 @@ fun PreviewTextItemViewQuote() {
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTextItemViewEmoji() {
|
||||
fun PreviewTextItemViewEmoji(@PreviewParameter(EditedProvider::class) edited: Boolean) {
|
||||
SimpleXTheme {
|
||||
FramedItemView(
|
||||
User.sampleData,
|
||||
@@ -139,7 +147,8 @@ fun PreviewTextItemViewEmoji() {
|
||||
Clock.System.now(),
|
||||
"👍",
|
||||
CIStatus.SndSent(),
|
||||
quotedItem = CIQuote.getSample(1, Clock.System.now(), "Lorem ipsum dolor sit amet", chatDir = CIDirection.DirectRcv())
|
||||
quotedItem = CIQuote.getSample(1, Clock.System.now(), "Lorem ipsum dolor sit amet", chatDir = CIDirection.DirectRcv()),
|
||||
itemEdited = edited
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ fun MarkdownText (
|
||||
formattedText: List<FormattedText>? = null,
|
||||
sender: String? = null,
|
||||
metaText: String? = null,
|
||||
edited: Boolean = false,
|
||||
style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp),
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
@@ -46,11 +47,12 @@ fun MarkdownText (
|
||||
senderBold: Boolean = false,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val reserve = if (edited) " " else " "
|
||||
if (formattedText == null) {
|
||||
val annotatedText = buildAnnotatedString {
|
||||
appendSender(this, sender, senderBold)
|
||||
append(content.text)
|
||||
if (metaText != null) withStyle(reserveTimestampStyle) { append(" $metaText") }
|
||||
if (metaText != null) withStyle(reserveTimestampStyle) { append(reserve + metaText) }
|
||||
}
|
||||
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow)
|
||||
} else {
|
||||
@@ -71,7 +73,7 @@ fun MarkdownText (
|
||||
}
|
||||
}
|
||||
}
|
||||
if (metaText != null) withStyle(reserveTimestampStyle) { append(" $metaText") }
|
||||
if (metaText != null) withStyle(reserveTimestampStyle) { append(reserve + metaText) }
|
||||
}
|
||||
if (hasLinks && uriHandler != null) {
|
||||
ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -8,6 +10,10 @@ import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.SupervisedUserCircle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -20,12 +26,32 @@ fun ChatInfoImage(chat: Chat, size: Dp) {
|
||||
val icon =
|
||||
if (chat.chatInfo is ChatInfo.Group) Icons.Filled.SupervisedUserCircle
|
||||
else Icons.Filled.AccountCircle
|
||||
ProfileImage(size, chat.chatInfo.image, icon)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileImage(
|
||||
size: Dp,
|
||||
image: String? = null,
|
||||
icon: ImageVector = Icons.Filled.AccountCircle
|
||||
) {
|
||||
Box(Modifier.size(size)) {
|
||||
Icon(icon,
|
||||
contentDescription = "Avatar Placeholder",
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
if (image == null) {
|
||||
Icon(
|
||||
icon,
|
||||
contentDescription = "profile image placeholder",
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
val imageBitmap = base64ToBitmap(image).asImageBitmap()
|
||||
Image(
|
||||
imageBitmap,
|
||||
"profile image",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(size).padding(size / 12).clip(CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.*
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Collections
|
||||
import androidx.compose.material.icons.outlined.PhotoCamera
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import chat.simplex.app.BuildConfig
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.views.newchat.ActionButton
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
|
||||
// Inspired by https://github.com/MakeItEasyDev/Jetpack-Compose-Capture-Image-Or-Choose-from-Gallery
|
||||
|
||||
fun bitmapToBase64(bitmap: Bitmap, squareCrop: Boolean = true): String {
|
||||
val size = 104
|
||||
var height = size
|
||||
var width = size
|
||||
var xOffset = 0
|
||||
var yOffset = 0
|
||||
if (bitmap.height < bitmap.width) {
|
||||
width = height * bitmap.width / bitmap.height
|
||||
xOffset = (width - height) / 2
|
||||
} else {
|
||||
height = width * bitmap.height / bitmap.width
|
||||
yOffset = (height - width) / 2
|
||||
}
|
||||
var image = bitmap
|
||||
while (image.width / 2 > width) {
|
||||
image = Bitmap.createScaledBitmap(image, image.width / 2, image.height / 2, true)
|
||||
}
|
||||
image = Bitmap.createScaledBitmap(image, width, height, true)
|
||||
if (squareCrop) {
|
||||
image = Bitmap.createBitmap(image, xOffset, yOffset, size, size)
|
||||
}
|
||||
val stream = ByteArrayOutputStream()
|
||||
image.compress(Bitmap.CompressFormat.JPEG, 85, stream)
|
||||
return "data:image/jpg;base64," + Base64.encodeToString(stream.toByteArray(), Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
fun base64ToBitmap(base64ImageString: String) : Bitmap {
|
||||
val imageString = base64ImageString
|
||||
.removePrefix("data:image/png;base64,")
|
||||
.removePrefix("data:image/jpg;base64,")
|
||||
val imageBytes = Base64.decode(imageString, Base64.NO_WRAP)
|
||||
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
}
|
||||
|
||||
class CustomTakePicturePreview : ActivityResultContract<Void?, Bitmap?>() {
|
||||
private var uri: Uri? = null
|
||||
private var tmpFile: File? = null
|
||||
lateinit var externalContext: Context
|
||||
|
||||
@CallSuper
|
||||
override fun createIntent(context: Context, input: Void?): Intent {
|
||||
externalContext = context
|
||||
tmpFile = File.createTempFile("image", ".bmp", context.filesDir)
|
||||
uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", tmpFile!!)
|
||||
return Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||
.putExtra(MediaStore.EXTRA_OUTPUT, uri)
|
||||
}
|
||||
|
||||
override fun getSynchronousResult(
|
||||
context: Context,
|
||||
input: Void?
|
||||
): SynchronousResult<Bitmap?>? = null
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Bitmap? {
|
||||
return if (resultCode == Activity.RESULT_OK && uri != null) {
|
||||
val source = ImageDecoder.createSource(externalContext.contentResolver, uri!!)
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
tmpFile?.delete()
|
||||
bitmap
|
||||
} else {
|
||||
Log.e( TAG, "Getting image from camera cancelled or failed.")
|
||||
tmpFile?.delete()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberGalleryLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLauncher<String, Uri?> =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent(), cb)
|
||||
|
||||
@Composable
|
||||
fun rememberCameraLauncher(cb: (Bitmap?) -> Unit): ManagedActivityResultLauncher<Void?, Bitmap?> =
|
||||
rememberLauncherForActivityResult(contract = CustomTakePicturePreview(), cb)
|
||||
|
||||
@Composable
|
||||
fun rememberPermissionLauncher(cb: (Boolean) -> Unit): ManagedActivityResultLauncher<String, Boolean> =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission(), cb)
|
||||
|
||||
@Composable
|
||||
fun GetImageBottomSheet(
|
||||
profileImageStr: MutableState<String?>,
|
||||
hideBottomSheet: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isCameraSelected = remember { mutableStateOf (false) }
|
||||
|
||||
val galleryLauncher = rememberGalleryLauncher { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
val source = ImageDecoder.createSource(context.contentResolver, uri)
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
profileImageStr.value = bitmapToBase64(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
val cameraLauncher = rememberCameraLauncher { bitmap: Bitmap? ->
|
||||
if (bitmap != null) profileImageStr.value = bitmapToBase64(bitmap)
|
||||
}
|
||||
|
||||
val permissionLauncher = rememberPermissionLauncher { isGranted: Boolean ->
|
||||
if (isGranted) {
|
||||
if (isCameraSelected.value) cameraLauncher.launch(null)
|
||||
else galleryLauncher.launch("image/*")
|
||||
hideBottomSheet()
|
||||
} else {
|
||||
Toast.makeText(context, "Permission Denied!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.onFocusChanged { focusState ->
|
||||
if (!focusState.hasFocus) hideBottomSheet()
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 30.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
ActionButton(null, "Use Camera", icon = Icons.Outlined.PhotoCamera) {
|
||||
when (PackageManager.PERMISSION_GRANTED) {
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) -> {
|
||||
cameraLauncher.launch(null)
|
||||
hideBottomSheet()
|
||||
}
|
||||
else -> {
|
||||
isCameraSelected.value = true
|
||||
permissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
}
|
||||
}
|
||||
ActionButton(null, "From Gallery", icon = Icons.Outlined.Collections) {
|
||||
when (PackageManager.PERMISSION_GRANTED) {
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) -> {
|
||||
galleryLauncher.launch("image/*")
|
||||
hideBottomSheet()
|
||||
}
|
||||
else -> {
|
||||
isCameraSelected.value = false
|
||||
permissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ fun NewChatSheetLayout(addContact: () -> Unit, scanCode: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionButton(text: String, comment: String, icon: ImageVector, disabled: Boolean = false,
|
||||
fun ActionButton(text: String?, comment: String?, icon: ImageVector, disabled: Boolean = false,
|
||||
click: () -> Unit = {}) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -97,16 +97,22 @@ fun ActionButton(text: String, comment: String, icon: ImageVector, disabled: Boo
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.padding(bottom = 8.dp))
|
||||
Text(text,
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = tint,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
Text(comment,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.body2
|
||||
)
|
||||
if (text != null) {
|
||||
Text(
|
||||
text,
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = tint,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
}
|
||||
if (comment != null) {
|
||||
Text(
|
||||
comment,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.body2
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
|
||||
@Composable
|
||||
fun MarkdownHelpView() {
|
||||
Column(Modifier.padding(horizontal = 16.dp)) {
|
||||
Column {
|
||||
Text(
|
||||
"How to use markdown",
|
||||
style = MaterialTheme.typography.h1,
|
||||
|
||||
@@ -23,6 +23,7 @@ import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.Profile
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.TerminalView
|
||||
import chat.simplex.app.views.helpers.ProfileImage
|
||||
import chat.simplex.app.views.newchat.ModalManager
|
||||
|
||||
@Composable
|
||||
@@ -32,6 +33,7 @@ fun SettingsView(chatModel: ChatModel) {
|
||||
SettingsLayout(
|
||||
profile = user.profile,
|
||||
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
|
||||
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
|
||||
showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } }
|
||||
)
|
||||
}
|
||||
@@ -44,6 +46,7 @@ val simplexTeamUri =
|
||||
fun SettingsLayout(
|
||||
profile: Profile,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
showTerminal: () -> Unit
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
@@ -66,11 +69,8 @@ fun SettingsLayout(
|
||||
)
|
||||
Spacer(Modifier.height(30.dp))
|
||||
|
||||
SettingsSectionView(showModal { UserProfileView(it) }, 60.dp) {
|
||||
Icon(
|
||||
Icons.Outlined.AccountCircle,
|
||||
contentDescription = "Avatar Placeholder",
|
||||
)
|
||||
SettingsSectionView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp) {
|
||||
ProfileImage(size = 60.dp, profile.image)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Column {
|
||||
Text(
|
||||
@@ -186,7 +186,7 @@ fun SettingsSectionView(click: () -> Unit, height: Dp = 48.dp, content: (@Compos
|
||||
.height(height),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
content.invoke()
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +202,7 @@ fun PreviewSettingsLayout() {
|
||||
SettingsLayout(
|
||||
profile = Profile.sampleData,
|
||||
showModal = {{}},
|
||||
showCustomModal = {{}},
|
||||
showTerminal = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
import android.widget.ScrollView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.PhotoCamera
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -17,29 +26,32 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.Profile
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import chat.simplex.app.views.chat.CIListState
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.ModalView
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun UserProfileView(chatModel: ChatModel) {
|
||||
fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
var editProfile by remember { mutableStateOf(false) }
|
||||
var editProfile = remember { mutableStateOf(false) }
|
||||
var profile by remember { mutableStateOf(user.profile) }
|
||||
UserProfileLayout(
|
||||
close = close,
|
||||
editProfile = editProfile,
|
||||
profile = profile,
|
||||
editProfileOff = { editProfile = false },
|
||||
editProfileOn = { editProfile = true },
|
||||
saveProfile = { displayName: String, fullName: String ->
|
||||
saveProfile = { displayName, fullName, image ->
|
||||
withApi {
|
||||
val newProfile = chatModel.controller.apiUpdateProfile(
|
||||
profile = Profile(displayName, fullName)
|
||||
)
|
||||
val p = Profile(displayName, fullName, image)
|
||||
val newProfile = chatModel.controller.apiUpdateProfile(p)
|
||||
if (newProfile != null) {
|
||||
chatModel.updateUserProfile(newProfile)
|
||||
profile = newProfile
|
||||
}
|
||||
editProfile = false
|
||||
editProfile.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -48,119 +60,192 @@ fun UserProfileView(chatModel: ChatModel) {
|
||||
|
||||
@Composable
|
||||
fun UserProfileLayout(
|
||||
editProfile: Boolean,
|
||||
close: () -> Unit,
|
||||
editProfile: MutableState<Boolean>,
|
||||
profile: Profile,
|
||||
editProfileOff: () -> Unit,
|
||||
editProfileOn: () -> Unit,
|
||||
saveProfile: (String, String) -> Unit,
|
||||
saveProfile: (String, String, String?) -> Unit,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.Start) {
|
||||
Text(
|
||||
"Your chat profile",
|
||||
Modifier.padding(bottom = 24.dp),
|
||||
style = MaterialTheme.typography.h1,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Text(
|
||||
"Your profile is stored on your device and shared only with your contacts.\n" +
|
||||
"SimpleX servers cannot see your profile.",
|
||||
Modifier.padding(bottom = 24.dp),
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
if (editProfile) {
|
||||
var displayName by remember { mutableStateOf(profile.displayName) }
|
||||
var fullName by remember { mutableStateOf(profile.fullName) }
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
// TODO hints
|
||||
BasicTextField(
|
||||
value = displayName,
|
||||
onValueChange = { displayName = it },
|
||||
modifier = Modifier
|
||||
.padding(bottom = 24.dp)
|
||||
.fillMaxWidth(),
|
||||
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
autoCorrect = false
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
BasicTextField(
|
||||
value = fullName,
|
||||
onValueChange = { fullName = it },
|
||||
modifier = Modifier
|
||||
.padding(bottom = 24.dp)
|
||||
.fillMaxWidth(),
|
||||
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
autoCorrect = false
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
Row {
|
||||
Text(
|
||||
"Cancel",
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = editProfileOff),
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 8.dp))
|
||||
Text(
|
||||
"Save (and notify contacts)",
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = { saveProfile(displayName, fullName) })
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(bottom = 24.dp)
|
||||
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val displayName = remember { mutableStateOf(profile.displayName) }
|
||||
val fullName = remember { mutableStateOf(profile.fullName) }
|
||||
val profileImage = remember { mutableStateOf(profile.image) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val keyboardState by getKeyboardState()
|
||||
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
|
||||
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
ModalBottomSheetLayout(
|
||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
sheetContent = {
|
||||
GetImageBottomSheet(profileImage, hideBottomSheet = {
|
||||
scope.launch { bottomSheetModalState.hide() }
|
||||
})
|
||||
},
|
||||
sheetState = bottomSheetModalState,
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||
) {
|
||||
ModalView(close = close) {
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.padding(bottom = 16.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
"Display name:",
|
||||
"Your chat profile",
|
||||
Modifier.padding(bottom = 24.dp),
|
||||
style = MaterialTheme.typography.h1,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
profile.displayName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
"Your profile is stored on your device and shared only with your contacts.\n\n" +
|
||||
"SimpleX servers cannot see your profile.",
|
||||
Modifier.padding(bottom = 24.dp),
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
if (editProfile.value) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(contentAlignment = Alignment.TopEnd) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
ProfileImage(192.dp, profileImage.value)
|
||||
EditImageButton { scope.launch { bottomSheetModalState.show() } }
|
||||
}
|
||||
if (profileImage.value != null) {
|
||||
DeleteImageButton { profileImage.value = null }
|
||||
}
|
||||
}
|
||||
}
|
||||
ProfileNameTextField(displayName)
|
||||
ProfileNameTextField(fullName)
|
||||
Row {
|
||||
TextButton("Cancel") {
|
||||
displayName.value = profile.displayName
|
||||
fullName.value = profile.fullName
|
||||
profileImage.value = profile.image
|
||||
editProfile.value = false
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 8.dp))
|
||||
TextButton("Save (and notify contacts)") {
|
||||
saveProfile(displayName.value, fullName.value, profileImage.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp), contentAlignment = Alignment.Center
|
||||
) {
|
||||
ProfileImage(192.dp, profile.image)
|
||||
if (profile.image == null) {
|
||||
EditImageButton {
|
||||
editProfile.value = true
|
||||
scope.launch { bottomSheetModalState.show() }
|
||||
}
|
||||
}
|
||||
}
|
||||
ProfileNameRow("Display name:", profile.displayName)
|
||||
ProfileNameRow("Full name:", profile.fullName)
|
||||
TextButton("Edit") { editProfile.value = true }
|
||||
}
|
||||
}
|
||||
if (savedKeyboardState != keyboardState) {
|
||||
LaunchedEffect(keyboardState) {
|
||||
scope.launch {
|
||||
savedKeyboardState = keyboardState
|
||||
scrollState.animateScrollTo(scrollState.maxValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
Modifier.padding(bottom = 24.dp)
|
||||
) {
|
||||
Text(
|
||||
"Full name:",
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
profile.fullName,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
Text(
|
||||
"Edit",
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = editProfileOn)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileNameTextField(name: MutableState<String>) {
|
||||
BasicTextField(
|
||||
value = name.value,
|
||||
onValueChange = { name.value = it },
|
||||
modifier = Modifier
|
||||
.padding(bottom = 24.dp)
|
||||
.fillMaxWidth(),
|
||||
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
autoCorrect = false
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileNameRow(label: String, text: String) {
|
||||
Row(Modifier.padding(bottom = 24.dp)) {
|
||||
Text(
|
||||
label,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
text,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextButton(text: String, click: () -> Unit) {
|
||||
Text(
|
||||
text,
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.clickable(onClick = click),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditImageButton(click: () -> Unit) {
|
||||
IconButton(
|
||||
onClick = click,
|
||||
modifier = Modifier.background(Color(1f, 1f, 1f, 0.2f), shape = CircleShape)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.PhotoCamera,
|
||||
contentDescription = "Edit image",
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.size(36.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteImageButton(click: () -> Unit) {
|
||||
IconButton(onClick = click) {
|
||||
Icon(
|
||||
Icons.Outlined.Close,
|
||||
contentDescription = "Delete image",
|
||||
tint = MaterialTheme.colors.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
@@ -171,11 +256,10 @@ fun UserProfileLayout(
|
||||
fun PreviewUserProfileLayoutEditOff() {
|
||||
SimpleXTheme {
|
||||
UserProfileLayout(
|
||||
close = {},
|
||||
profile = Profile.sampleData,
|
||||
editProfile = false,
|
||||
editProfileOff = {},
|
||||
editProfileOn = {},
|
||||
saveProfile = { _, _ -> }
|
||||
editProfile = remember { mutableStateOf(false) },
|
||||
saveProfile = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -190,11 +274,10 @@ fun PreviewUserProfileLayoutEditOff() {
|
||||
fun PreviewUserProfileLayoutEditOn() {
|
||||
SimpleXTheme {
|
||||
UserProfileLayout(
|
||||
close = {},
|
||||
profile = Profile.sampleData,
|
||||
editProfile = true,
|
||||
editProfileOff = {},
|
||||
editProfileOn = {},
|
||||
saveProfile = { _, _ -> }
|
||||
editProfile = remember { mutableStateOf(true) },
|
||||
saveProfile = {_, _, _ ->}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/android/app/src/main/res/xml/file_paths.xml
Normal file
3
apps/android/app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<paths>
|
||||
<files-path name="my_files" path="/"/>
|
||||
</paths>
|
||||
@@ -190,8 +190,8 @@ struct User: Decodable, NamedChat {
|
||||
var activeUser: Bool
|
||||
|
||||
var displayName: String { get { profile.displayName } }
|
||||
|
||||
var fullName: String { get { profile.fullName } }
|
||||
var image: String? { get { profile.image } }
|
||||
|
||||
static let sampleData = User(
|
||||
userId: 1,
|
||||
@@ -209,6 +209,7 @@ typealias GroupName = String
|
||||
struct Profile: Codable, NamedChat {
|
||||
var displayName: String
|
||||
var fullName: String
|
||||
var image: String?
|
||||
|
||||
static let sampleData = Profile(
|
||||
displayName: "alice",
|
||||
@@ -225,6 +226,7 @@ enum ChatType: String {
|
||||
protocol NamedChat {
|
||||
var displayName: String { get }
|
||||
var fullName: String { get }
|
||||
var image: String? { get }
|
||||
}
|
||||
|
||||
extension NamedChat {
|
||||
@@ -270,6 +272,16 @@ enum ChatInfo: Identifiable, Decodable, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
var image: String? {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.image
|
||||
case let .group(groupInfo): return groupInfo.image
|
||||
case let .contactRequest(contactRequest): return contactRequest.image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var id: ChatId {
|
||||
get {
|
||||
switch self {
|
||||
@@ -420,6 +432,7 @@ struct Contact: Identifiable, Decodable, NamedChat {
|
||||
var ready: Bool { get { activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" } }
|
||||
var displayName: String { get { profile.displayName } }
|
||||
var fullName: String { get { profile.fullName } }
|
||||
var image: String? { get { profile.image } }
|
||||
|
||||
static let sampleData = Contact(
|
||||
contactId: 1,
|
||||
@@ -452,6 +465,7 @@ struct UserContactRequest: Decodable, NamedChat {
|
||||
var ready: Bool { get { true } }
|
||||
var displayName: String { get { profile.displayName } }
|
||||
var fullName: String { get { profile.fullName } }
|
||||
var image: String? { get { profile.image } }
|
||||
|
||||
static let sampleData = UserContactRequest(
|
||||
contactRequestId: 1,
|
||||
@@ -472,6 +486,7 @@ struct GroupInfo: Identifiable, Decodable, NamedChat {
|
||||
var ready: Bool { get { true } }
|
||||
var displayName: String { get { groupProfile.displayName } }
|
||||
var fullName: String { get { groupProfile.fullName } }
|
||||
var image: String? { get { groupProfile.image } }
|
||||
|
||||
static let sampleData = GroupInfo(
|
||||
groupId: 1,
|
||||
@@ -484,6 +499,7 @@ struct GroupInfo: Identifiable, Decodable, NamedChat {
|
||||
struct GroupProfile: Codable, NamedChat {
|
||||
var displayName: String
|
||||
var fullName: String
|
||||
var image: String?
|
||||
|
||||
static let sampleData = GroupProfile(
|
||||
displayName: "team",
|
||||
@@ -548,10 +564,10 @@ struct ChatItem: Identifiable, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil) -> ChatItem {
|
||||
static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> ChatItem {
|
||||
ChatItem(
|
||||
chatDir: dir,
|
||||
meta: CIMeta.getSample(id, ts, text, status),
|
||||
meta: CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable),
|
||||
content: .sndMsgContent(msgContent: .text(text)),
|
||||
quotedItem: quotedItem
|
||||
)
|
||||
@@ -582,16 +598,22 @@ struct CIMeta: Decodable {
|
||||
var itemText: String
|
||||
var itemStatus: CIStatus
|
||||
var createdAt: Date
|
||||
var itemDeleted: Bool
|
||||
var itemEdited: Bool
|
||||
var editable: Bool
|
||||
|
||||
var timestampText: Text { get { SimpleX.timestampText(itemTs) } }
|
||||
|
||||
static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew) -> CIMeta {
|
||||
static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> CIMeta {
|
||||
CIMeta(
|
||||
itemId: id,
|
||||
itemTs: ts,
|
||||
itemText: text,
|
||||
itemStatus: status,
|
||||
createdAt: ts
|
||||
createdAt: ts,
|
||||
itemDeleted: itemDeleted,
|
||||
itemEdited: itemEdited,
|
||||
editable: editable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ private var chatController: chat_ctrl?
|
||||
private let jsonDecoder = getJSONDecoder()
|
||||
private let jsonEncoder = getJSONEncoder()
|
||||
|
||||
enum MsgDeleteMode: String {
|
||||
case mdBroadcast = "broadcast"
|
||||
case mdInternal = "internal"
|
||||
}
|
||||
|
||||
enum ChatCommand {
|
||||
case showActiveUser
|
||||
case createActiveUser(profile: Profile)
|
||||
@@ -23,12 +28,14 @@ enum ChatCommand {
|
||||
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 apiUpdateMessage(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent)
|
||||
case apiDeleteMessage(type: ChatType, id: Int64, itemId: Int64, mode: MsgDeleteMode)
|
||||
case getUserSMPServers
|
||||
case setUserSMPServers(smpServers: [String])
|
||||
case addContact
|
||||
case connect(connReq: String)
|
||||
case apiDeleteChat(type: ChatType, id: Int64)
|
||||
case updateProfile(profile: Profile)
|
||||
case apiUpdateProfile(profile: Profile)
|
||||
case createMyAddress
|
||||
case deleteMyAddress
|
||||
case showMyAddress
|
||||
@@ -47,12 +54,14 @@ enum ChatCommand {
|
||||
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 let .apiUpdateMessage(type, id, itemId, mc): return "/_update item \(ref(type, id)) \(itemId) \(mc.cmdString)"
|
||||
case let .apiDeleteMessage(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
|
||||
case .getUserSMPServers: return "/smp_servers"
|
||||
case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))"
|
||||
case .addContact: return "/connect"
|
||||
case let .connect(connReq): return "/connect \(connReq)"
|
||||
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
|
||||
case let .updateProfile(profile): return "/profile \(profile.displayName) \(profile.fullName)"
|
||||
case let .apiUpdateProfile(profile): return "/_profile \(encodeJSON(profile))"
|
||||
case .createMyAddress: return "/address"
|
||||
case .deleteMyAddress: return "/delete_address"
|
||||
case .showMyAddress: return "/show_address"
|
||||
@@ -74,12 +83,14 @@ enum ChatCommand {
|
||||
case .apiGetChat: return "apiGetChat"
|
||||
case .apiSendMessage: return "apiSendMessage"
|
||||
case .apiSendMessageQuote: return "apiSendMessageQuote"
|
||||
case .apiUpdateMessage: return "apiUpdateMessage"
|
||||
case .apiDeleteMessage: return "apiDeleteMessage"
|
||||
case .getUserSMPServers: return "getUserSMPServers"
|
||||
case .setUserSMPServers: return "setUserSMPServers"
|
||||
case .addContact: return "addContact"
|
||||
case .connect: return "connect"
|
||||
case .apiDeleteChat: return "apiDeleteChat"
|
||||
case .updateProfile: return "updateProfile"
|
||||
case .apiUpdateProfile: return "apiUpdateProfile"
|
||||
case .createMyAddress: return "createMyAddress"
|
||||
case .deleteMyAddress: return "deleteMyAddress"
|
||||
case .showMyAddress: return "showMyAddress"
|
||||
@@ -135,7 +146,9 @@ enum ChatResponse: Decodable, Error {
|
||||
case groupEmpty(groupInfo: GroupInfo)
|
||||
case userContactLinkSubscribed
|
||||
case newChatItem(chatItem: AChatItem)
|
||||
case chatItemStatusUpdated(chatItem: AChatItem)
|
||||
case chatItemUpdated(chatItem: AChatItem)
|
||||
case chatItemDeleted(chatItem: AChatItem)
|
||||
case cmdOk
|
||||
case chatCmdError(chatError: ChatError)
|
||||
case chatError(chatError: ChatError)
|
||||
@@ -155,7 +168,7 @@ enum ChatResponse: Decodable, Error {
|
||||
case .sentInvitation: return "sentInvitation"
|
||||
case .contactDeleted: return "contactDeleted"
|
||||
case .userProfileNoChange: return "userProfileNoChange"
|
||||
case .userProfileUpdated: return "userProfileNoChange"
|
||||
case .userProfileUpdated: return "userProfileUpdated"
|
||||
case .userContactLink: return "userContactLink"
|
||||
case .userContactLinkCreated: return "userContactLinkCreated"
|
||||
case .userContactLinkDeleted: return "userContactLinkDeleted"
|
||||
@@ -173,7 +186,9 @@ enum ChatResponse: Decodable, Error {
|
||||
case .groupEmpty: return "groupEmpty"
|
||||
case .userContactLinkSubscribed: return "userContactLinkSubscribed"
|
||||
case .newChatItem: return "newChatItem"
|
||||
case .chatItemStatusUpdated: return "chatItemStatusUpdated"
|
||||
case .chatItemUpdated: return "chatItemUpdated"
|
||||
case .chatItemDeleted: return "chatItemDeleted"
|
||||
case .cmdOk: return "cmdOk"
|
||||
case .chatCmdError: return "chatCmdError"
|
||||
case .chatError: return "chatError"
|
||||
@@ -214,7 +229,9 @@ enum ChatResponse: Decodable, Error {
|
||||
case let .groupEmpty(groupInfo): return String(describing: groupInfo)
|
||||
case .userContactLinkSubscribed: return noDetails
|
||||
case let .newChatItem(chatItem): return String(describing: chatItem)
|
||||
case let .chatItemStatusUpdated(chatItem): return String(describing: chatItem)
|
||||
case let .chatItemUpdated(chatItem): return String(describing: chatItem)
|
||||
case let .chatItemDeleted(chatItem): return String(describing: chatItem)
|
||||
case .cmdOk: return noDetails
|
||||
case let .chatCmdError(chatError): return String(describing: chatError)
|
||||
case let .chatError(chatError): return String(describing: chatError)
|
||||
@@ -393,6 +410,18 @@ func apiSendMessage(type: ChatType, id: Int64, quotedItemId: Int64?, msg: MsgCon
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiUpdateMessage(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) async throws -> ChatItem {
|
||||
let r = await chatSendCmd(.apiUpdateMessage(type: type, id: id, itemId: itemId, msg: msg), bgDelay: msgDelay)
|
||||
if case let .chatItemUpdated(aChatItem) = r { return aChatItem.chatItem }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteMessage(type: ChatType, id: Int64, itemId: Int64, mode: MsgDeleteMode) async throws -> ChatItem {
|
||||
let r = await chatSendCmd(.apiDeleteMessage(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay)
|
||||
if case let .chatItemUpdated(aChatItem) = r { return aChatItem.chatItem }
|
||||
throw r
|
||||
}
|
||||
|
||||
func getUserSMPServers() throws -> [String] {
|
||||
let r = chatSendCmdSync(.getUserSMPServers)
|
||||
if case let .userSMPServers(smpServers) = r { return smpServers }
|
||||
@@ -427,7 +456,7 @@ func apiDeleteChat(type: ChatType, id: Int64) async throws {
|
||||
}
|
||||
|
||||
func apiUpdateProfile(profile: Profile) async throws -> Profile? {
|
||||
let r = await chatSendCmd(.updateProfile(profile: profile))
|
||||
let r = await chatSendCmd(.apiUpdateProfile(profile: profile))
|
||||
switch r {
|
||||
case .userProfileNoChange: return nil
|
||||
case let .userProfileUpdated(_, toProfile): return toProfile
|
||||
@@ -601,7 +630,7 @@ func processReceivedMsg(_ res: ChatResponse) {
|
||||
let cItem = aChatItem.chatItem
|
||||
chatModel.addChatItem(cInfo, cItem)
|
||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
case let .chatItemUpdated(aChatItem):
|
||||
case let .chatItemStatusUpdated(aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
if chatModel.upsertChatItem(cInfo, cItem) {
|
||||
@@ -614,6 +643,15 @@ func processReceivedMsg(_ res: ChatResponse) {
|
||||
default: break
|
||||
}
|
||||
}
|
||||
case let .chatItemUpdated(aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
if chatModel.upsertChatItem(cInfo, cItem) {
|
||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
}
|
||||
case .chatItemDeleted(_):
|
||||
// TODO let .chatItemDeleted(aChatItem)
|
||||
return
|
||||
default:
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
}
|
||||
@@ -703,10 +741,13 @@ private func getJSONObject(_ cjson: UnsafePointer<CChar>) -> NSDictionary? {
|
||||
return try? JSONSerialization.jsonObject(with: d) as? NSDictionary
|
||||
}
|
||||
|
||||
private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
|
||||
private func encodeJSON<T: Encodable>(_ value: T) -> String {
|
||||
let data = try! jsonEncoder.encode(value)
|
||||
let str = String(decoding: data, as: UTF8.self)
|
||||
return str.cString(using: .utf8)!
|
||||
return String(decoding: data, as: UTF8.self)
|
||||
}
|
||||
|
||||
private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
|
||||
encodeJSON(value).cString(using: .utf8)!
|
||||
}
|
||||
|
||||
enum ChatError: Decodable {
|
||||
@@ -742,6 +783,8 @@ enum ChatErrorType: Decodable {
|
||||
case fileSend(fileId: Int64, agentError: String)
|
||||
case fileRcvChunk(message: String)
|
||||
case fileInternal(message: String)
|
||||
case invalidQuote
|
||||
case invalidMessageUpdate
|
||||
case agentVersion
|
||||
case commandError(message: String)
|
||||
}
|
||||
@@ -773,6 +816,8 @@ enum StoreError: Decodable {
|
||||
case noMsgDelivery(connId: Int64, agentMsgId: String)
|
||||
case badChatItem(itemId: Int64)
|
||||
case chatItemNotFound(itemId: Int64)
|
||||
case quotedChatItemNotFound
|
||||
case chatItemSharedMsgIdNotFound(sharedMsgId: String)
|
||||
}
|
||||
|
||||
enum AgentErrorType: Decodable {
|
||||
|
||||
@@ -13,6 +13,10 @@ struct CIMetaView: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
if chatItem.meta.itemEdited {
|
||||
statusImage("pencil", .secondary, 9)
|
||||
}
|
||||
|
||||
switch chatItem.meta.itemStatus {
|
||||
case .sndSent:
|
||||
statusImage("checkmark", .secondary)
|
||||
@@ -31,17 +35,20 @@ struct CIMetaView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func statusImage(_ systemName: String, _ color: Color) -> some View {
|
||||
private func statusImage(_ systemName: String, _ color: Color, _ maxHeight: CGFloat = 8) -> some View {
|
||||
Image(systemName: systemName)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.foregroundColor(color)
|
||||
.frame(maxHeight: 8)
|
||||
.frame(maxHeight: maxHeight)
|
||||
}
|
||||
}
|
||||
|
||||
struct CIMetaView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent))
|
||||
return Group {
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent))
|
||||
CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, false, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,8 @@ struct FramedItemView: View {
|
||||
content: chatItem.content,
|
||||
formattedText: chatItem.formattedText,
|
||||
sender: chatItem.memberDisplayName,
|
||||
metaText: chatItem.timestampText
|
||||
metaText: chatItem.timestampText,
|
||||
edited: chatItem.meta.itemEdited
|
||||
)
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 12)
|
||||
@@ -63,14 +64,15 @@ struct FramedItemView: View {
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
CIMetaView(chatItem: chatItem)
|
||||
.padding(.trailing, 12)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.bottom, 6)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
.background(chatItemFrameColor(chatItem, colorScheme))
|
||||
.cornerRadius(18)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
switch chatItem.meta.itemStatus {
|
||||
case .sndErrorAuth:
|
||||
@@ -110,3 +112,19 @@ struct FramedItemView_Previews: PreviewProvider {
|
||||
.previewLayout(.fixed(width: 360, height: 200))
|
||||
}
|
||||
}
|
||||
|
||||
struct FramedItemViewEdited_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group{
|
||||
FramedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, false, true))
|
||||
FramedItemView(chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), false, true))
|
||||
FramedItemView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), false, true))
|
||||
FramedItemView(chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), false, true))
|
||||
FramedItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, false, true))
|
||||
FramedItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, false, true))
|
||||
FramedItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, false, true))
|
||||
FramedItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, false, true))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 200))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,20 +16,22 @@ struct MsgContentView: View {
|
||||
var formattedText: [FormattedText]? = nil
|
||||
var sender: String? = nil
|
||||
var metaText: Text? = nil
|
||||
var edited: Bool = false
|
||||
|
||||
var body: some View {
|
||||
var body: some View {
|
||||
let v = messageText(content, formattedText, sender)
|
||||
if let mt = metaText {
|
||||
return v + reserveSpaceForMeta(mt)
|
||||
return v + reserveSpaceForMeta(mt, edited)
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
private func reserveSpaceForMeta(_ meta: Text) -> Text {
|
||||
(Text(" ") + meta)
|
||||
.font(.caption)
|
||||
.foregroundColor(.clear)
|
||||
private func reserveSpaceForMeta(_ meta: Text, _ edited: Bool) -> Text {
|
||||
let reserve = edited ? " " : " "
|
||||
return (Text(reserve) + meta)
|
||||
.font(.caption)
|
||||
.foregroundColor(.clear)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
@State var message: String = ""
|
||||
@State var quotedItem: ChatItem? = nil
|
||||
@State var editingItem: ChatItem? = nil
|
||||
@State private var inProgress: Bool = false
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@State private var showChatInfo = false
|
||||
@@ -31,7 +33,10 @@ struct ChatView: View {
|
||||
ChatItemView(chatItem: ci)
|
||||
.contextMenu {
|
||||
Button {
|
||||
withAnimation { quotedItem = ci }
|
||||
withAnimation {
|
||||
editingItem = nil
|
||||
quotedItem = ci
|
||||
}
|
||||
} label: { Label("Reply", systemImage: "arrowshape.turn.up.left") }
|
||||
Button {
|
||||
showShareSheet(items: [ci.content.text])
|
||||
@@ -39,6 +44,15 @@ struct ChatView: View {
|
||||
Button {
|
||||
UIPasteboard.general.string = ci.content.text
|
||||
} label: { Label("Copy", systemImage: "doc.on.doc") }
|
||||
// if (ci.chatDir.sent && ci.meta.editable) {
|
||||
// Button {
|
||||
// withAnimation {
|
||||
// quotedItem = nil
|
||||
// editingItem = ci
|
||||
// message = ci.content.text
|
||||
// }
|
||||
// } label: { Label("Edit", systemImage: "square.and.pencil") }
|
||||
// }
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment)
|
||||
@@ -71,8 +85,11 @@ struct ChatView: View {
|
||||
Spacer(minLength: 0)
|
||||
|
||||
ComposeView(
|
||||
message: $message,
|
||||
quotedItem: $quotedItem,
|
||||
editingItem: $editingItem,
|
||||
sendMessage: sendMessage,
|
||||
resetMessage: { message = "" },
|
||||
inProgress: inProgress,
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
@@ -130,20 +147,35 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
func sendMessage(_ msg: String) {
|
||||
logger.debug("ChatView sendMessage")
|
||||
Task {
|
||||
logger.debug("ChatView sendMessage: in Task")
|
||||
do {
|
||||
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)
|
||||
if let ei = editingItem {
|
||||
let chatItem = try await apiUpdateMessage(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
itemId: ei.id,
|
||||
msg: .text(msg)
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
editingItem = nil
|
||||
let _ = chatModel.upsertChatItem(chat.chatInfo, chatItem)
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
logger.error("ChatView.sendMessage apiSendMessage error: \(error.localizedDescription)")
|
||||
logger.error("ChatView.sendMessage error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,35 +8,69 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// TODO
|
||||
//enum ComposeState {
|
||||
// case plain
|
||||
// case quoted(quotedItem: ChatItem)
|
||||
// case editing(editingItem: ChatItem)
|
||||
//}
|
||||
|
||||
struct ComposeView: View {
|
||||
@Binding var message: String
|
||||
@Binding var quotedItem: ChatItem?
|
||||
@Binding var editingItem: ChatItem?
|
||||
var sendMessage: (String) -> Void
|
||||
var resetMessage: () -> Void
|
||||
var inProgress: Bool = false
|
||||
@FocusState.Binding var keyboardVisible: Bool
|
||||
@State var editing: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
QuotedItemView(quotedItem: $quotedItem)
|
||||
.transition(.move(edge: .bottom))
|
||||
if (quotedItem != nil) {
|
||||
ContextItemView(contextItem: $quotedItem, editing: $editing)
|
||||
} else if (editingItem != nil) {
|
||||
ContextItemView(contextItem: $editingItem, editing: $editing, resetMessage: resetMessage)
|
||||
}
|
||||
SendMessageView(
|
||||
sendMessage: sendMessage,
|
||||
inProgress: inProgress,
|
||||
keyboardVisible: $keyboardVisible
|
||||
message: $message,
|
||||
keyboardVisible: $keyboardVisible,
|
||||
editing: $editing
|
||||
)
|
||||
.background(.background)
|
||||
}
|
||||
.onChange(of: editingItem == nil) { _ in
|
||||
editing = (editingItem != nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var message: String = ""
|
||||
@FocusState var keyboardVisible: Bool
|
||||
@State var quotedItem: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello")
|
||||
@State var item: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello")
|
||||
@State var nilItem: ChatItem? = nil
|
||||
|
||||
return ComposeView(
|
||||
quotedItem: $quotedItem,
|
||||
sendMessage: { print ($0) },
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
return Group {
|
||||
ComposeView(
|
||||
message: $message,
|
||||
quotedItem: $item,
|
||||
editingItem: $nilItem,
|
||||
sendMessage: { print ($0) },
|
||||
resetMessage: {},
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
ComposeView(
|
||||
message: $message,
|
||||
quotedItem: $nilItem,
|
||||
editingItem: $item,
|
||||
sendMessage: { print ($0) },
|
||||
resetMessage: {},
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// ContextItemView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by JRoberts on 13/03/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContextItemView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Binding var contextItem: ChatItem?
|
||||
@Binding var editing: Bool
|
||||
var resetMessage: () -> Void = {}
|
||||
|
||||
var body: some View {
|
||||
if let cxtItem = contextItem {
|
||||
HStack {
|
||||
contextText(cxtItem).lineLimit(3)
|
||||
Spacer()
|
||||
Button {
|
||||
withAnimation {
|
||||
contextItem = nil
|
||||
if editing { resetMessage() }
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "multiply")
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(chatItemFrameColor(cxtItem, colorScheme))
|
||||
.padding(.top, 8)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
func contextText(_ cxtItem: ChatItem) -> some View {
|
||||
if let s = cxtItem.memberDisplayName {
|
||||
return (Text(s).fontWeight(.medium) + Text(": \(cxtItem.content.text)"))
|
||||
} else {
|
||||
return Text(cxtItem.content.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var contextItem: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello")
|
||||
@State var editing: Bool = false
|
||||
return ContextItemView(contextItem: $contextItem, editing: $editing)
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,10 @@ import SwiftUI
|
||||
struct SendMessageView: View {
|
||||
var sendMessage: (String) -> Void
|
||||
var inProgress: Bool = false
|
||||
@State private var message: String = "" //Lorem ipsum dolor sit amet, consectetur" // adipiscing elit, sed do eiusmod tempor incididunt ut labor7 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."
|
||||
@Binding var message: String //Lorem ipsum dolor sit amet, consectetur" // adipiscing elit, sed do eiusmod tempor incididunt ut labor7 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."
|
||||
@Namespace var namespace
|
||||
@FocusState.Binding var keyboardVisible: Bool
|
||||
@Binding var editing: Bool
|
||||
@State private var teHeight: CGFloat = 42
|
||||
@State private var teFont: Font = .body
|
||||
var maxHeight: CGFloat = 360
|
||||
@@ -47,7 +48,7 @@ struct SendMessageView: View {
|
||||
.padding([.bottom, .trailing], 3)
|
||||
} else {
|
||||
Button(action: submit) {
|
||||
Image(systemName: "arrow.up.circle.fill")
|
||||
Image(systemName: editing ? "checkmark.circle.fill" : "arrow.up.circle.fill")
|
||||
.resizable()
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
@@ -85,15 +86,34 @@ struct SendMessageView: View {
|
||||
|
||||
struct SendMessageView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var message: String = ""
|
||||
@FocusState var keyboardVisible: Bool
|
||||
@State var editingOff: Bool = false
|
||||
@State var editingOn: Bool = true
|
||||
@State var item: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello")
|
||||
@State var nilItem: ChatItem? = nil
|
||||
|
||||
return VStack {
|
||||
Text("")
|
||||
Spacer(minLength: 0)
|
||||
SendMessageView(
|
||||
sendMessage: { print ($0) },
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
return Group {
|
||||
VStack {
|
||||
Text("")
|
||||
Spacer(minLength: 0)
|
||||
SendMessageView(
|
||||
sendMessage: { print ($0) },
|
||||
message: $message,
|
||||
keyboardVisible: $keyboardVisible,
|
||||
editing: $editingOff
|
||||
)
|
||||
}
|
||||
VStack {
|
||||
Text("")
|
||||
Spacer(minLength: 0)
|
||||
SendMessageView(
|
||||
sendMessage: { print ($0) },
|
||||
message: $message,
|
||||
keyboardVisible: $keyboardVisible,
|
||||
editing: $editingOn
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,11 @@ struct ChatInfoImage: View {
|
||||
case .group: iconName = "person.2.circle.fill"
|
||||
default: iconName = "circle.fill"
|
||||
}
|
||||
|
||||
return Image(systemName: iconName)
|
||||
.resizable()
|
||||
.foregroundColor(color)
|
||||
return ProfileImage(
|
||||
imageStr: chat.chatInfo.image,
|
||||
iconName: iconName,
|
||||
color: color
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ import SwiftUI
|
||||
struct DetermineWidth: View {
|
||||
typealias Key = MaximumWidthPreferenceKey
|
||||
var body: some View {
|
||||
GeometryReader {
|
||||
proxy in
|
||||
GeometryReader { proxy in
|
||||
Color.clear
|
||||
.anchorPreference(key: Key.self, value: .bounds) {
|
||||
anchor in proxy[anchor].size.width
|
||||
}
|
||||
.preference(
|
||||
key: MaximumWidthPreferenceKey.self,
|
||||
value: proxy.size.width
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
apps/ios/Shared/Views/Helpers/ImagePicker.swift
Normal file
48
apps/ios/Shared/Views/Helpers/ImagePicker.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// ImagePicker.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny on 23/03/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ImagePicker: UIViewControllerRepresentable {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
var source: UIImagePickerController.SourceType
|
||||
@Binding var image: UIImage?
|
||||
|
||||
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
|
||||
let parent: ImagePicker
|
||||
|
||||
init(_ parent: ImagePicker) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController,
|
||||
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||
if let uiImage = info[.originalImage] as? UIImage {
|
||||
parent.image = uiImage
|
||||
}
|
||||
parent.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
|
||||
let picker = UIImagePickerController()
|
||||
picker.sourceType = source
|
||||
picker.allowsEditing = false
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
|
||||
|
||||
}
|
||||
}
|
||||
45
apps/ios/Shared/Views/Helpers/ProfileImage.swift
Normal file
45
apps/ios/Shared/Views/Helpers/ProfileImage.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// ProfileImage.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny on 23/03/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ProfileImage: View {
|
||||
var imageStr: String? = nil
|
||||
var iconName: String = "person.crop.circle.fill"
|
||||
var color = Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
|
||||
var body: some View {
|
||||
if let image = imageStr,
|
||||
let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let uiImage = UIImage(data: data) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
Image(systemName: iconName)
|
||||
.resizable()
|
||||
.foregroundColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
func dropPrefix(_ s: String, _ prefix: String) -> String {
|
||||
s.hasPrefix(prefix) ? String(s.dropFirst(prefix.count)) : s
|
||||
}
|
||||
|
||||
func dropImagePrefix(_ s: String) -> String {
|
||||
dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,")
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfileImage_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ProfileImage(imageStr: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAKHGlDQ1BJQ0MgUHJvZmlsZQAASImFVgdUVNcWve9Nb7QZeu9NehtAem/Sq6gMQ28OQxWxgAQjEFFEREARNFQFg1KjiIhiIQgoYA9IEFBisCAq6OQNJNH4//r/zDpz9ttzz7n73ffWmg0A6QCDxYqD+QCIT0hmezlYywQEBsngngEYCAIy0AC6DGYSy8rDwxUg8Xf9d7wbAxC33tHgzvrP3/9nCISFJzEBgIIRTGey2MkILkawT1oyi4tnEUxjI6IQvMLFkauYqxjQQtewwuoaHy8bBNMBwJMZDHYkAERbhJdJZUYic4hhCNZOCItOQDB3vjkzioFwxLsIXhcRl5IOAImrRzs+fivCk7QRrIL0shAcwNUW+tX8yH/tFfrPXgxG5D84Pi6F+dc9ck+HHJ7g641UMSQlQATQBHEgBaQDGcACbLAVYaIRJhx5Dv+9j77aZ4OsZIFtSEc0iARRIBnpt/9qlvfqpGSQBhjImnCEcUU+NtxnujZy4fbqVEiU/wuXdQyA9S0cDqfzC+e2F4DzyLkSB79wyi0A8KoBcL2GmcJOXePQ3C8MIAJeQAOiQArIAxXuWwMMgSmwBHbAGbgDHxAINgMmojceUZUGMkEWyAX54AA4DMpAJTgJ6sAZ0ALawQVwGVwDt8AQGAUPwQSYBi/AAngHliEIwkEUiAqJQtKQIqQO6UJ0yByyg1whLygQCoEioQQoBcqE9kD5UBFUBlVB9dBPUCd0GboBDUP3oUloDnoNfYRRMBmmwZKwEqwF02Er2AX2gTfBkXAinAHnwPvhUrgaPg23wZfhW/AoPAG/gBdRAEVCCaFkURooOsoG5Y4KQkWg2KidqDxUCaoa1YTqQvWj7qAmUPOoD2gsmoqWQWugTdGOaF80E52I3okuQJeh69Bt6D70HfQkegH9GUPBSGDUMSYYJ0wAJhKThsnFlGBqMK2Yq5hRzDTmHRaLFcIqY42wjthAbAx2O7YAewzbjO3BDmOnsIs4HE4Up44zw7njGLhkXC7uKO407hJuBDeNe48n4aXxunh7fBA+AZ+NL8E34LvxI/gZ/DKBj6BIMCG4E8II2wiFhFOELsJtwjRhmchPVCaaEX2IMcQsYimxiXiV+Ij4hkQiyZGMSZ6kaNJuUinpLOk6aZL0gSxAViPbkIPJKeT95FpyD/k++Q2FQlGiWFKCKMmU/ZR6yhXKE8p7HiqPJo8TTxjPLp5ynjaeEZ6XvAReRV4r3s28GbwlvOd4b/PO8xH4lPhs+Bh8O/nK+Tr5xvkW+an8Ovzu/PH8BfwN/Df4ZwVwAkoCdgJhAjkCJwWuCExRUVR5qg2VSd1DPUW9Sp2mYWnKNCdaDC2fdoY2SFsQFBDUF/QTTBcsF7woOCGEElISchKKEyoUahEaE/ooLClsJRwuvE+4SXhEeElEXMRSJFwkT6RZZFTko6iMqJ1orOhB0XbRx2JoMTUxT7E0seNiV8XmxWnipuJM8TzxFvEHErCEmoSXxHaJkxIDEouSUpIOkizJo5JXJOelhKQspWKkiqW6peakqdLm0tHSxdKXpJ/LCMpYycTJlMr0ySzISsg6yqbIVskOyi7LKcv5ymXLNcs9lifK0+Uj5Ivle+UXFKQV3BQyFRoVHigSFOmKUYpHFPsVl5SUlfyV9iq1K80qiyg7KWcoNyo/UqGoWKgkqlSr3FXFqtJVY1WPqQ6pwWoGalFq5Wq31WF1Q/Vo9WPqw+sw64zXJayrXjeuQdaw0kjVaNSY1BTSdNXM1mzXfKmloBWkdVCrX+uztoF2nPYp7Yc6AjrOOtk6XTqvddV0mbrlunf1KHr2erv0OvRe6avrh+sf179nQDVwM9hr0GvwydDIkG3YZDhnpGAUYlRhNE6n0T3oBfTrxhhja+NdxheMP5gYmiSbtJj8YaphGmvaYDq7Xnl9+PpT66fM5MwYZlVmE+Yy5iHmJ8wnLGQtGBbVFk8t5S3DLGssZ6xUrWKsTlu9tNa2Zlu3Wi/ZmNjssOmxRdk62ObZDtoJ2Pnaldk9sZezj7RvtF9wMHDY7tDjiHF0cTzoOO4k6cR0qndacDZy3uHc50J28XYpc3nqqubKdu1yg92c3Q65PdqguCFhQ7s7cHdyP+T+2EPZI9HjZ0+sp4dnueczLx2vTK9+b6r3Fu8G73c+1j6FPg99VXxTfHv9eP2C/er9lvxt/Yv8JwK0AnYE3AoUC4wO7AjCBfkF1QQtbrTbeHjjdLBBcG7w2CblTembbmwW2xy3+eIW3i2MLedCMCH+IQ0hKwx3RjVjMdQptCJ0gWnDPMJ8EWYZVhw2F24WXhQ+E2EWURQxG2kWeShyLsoiqiRqPtomuiz6VYxjTGXMUqx7bG0sJ84/rjkeHx8S35kgkBCb0LdVamv61mGWOiuXNZFokng4cYHtwq5JgpI2JXUk05A/0oEUlZTvUiZTzVPLU9+n+aWdS+dPT0gf2Ka2bd+2mQz7jB+3o7czt/dmymZmZU7usNpRtRPaGbqzd5f8rpxd07sddtdlEbNis37J1s4uyn67x39PV45kzu6cqe8cvmvM5cll547vNd1b+T36++jvB/fp7Tu673NeWN7NfO38kvyVAmbBzR90fij9gbM/Yv9goWHh8QPYAwkHxg5aHKwr4i/KKJo65HaorVimOK/47eEth2+U6JdUHiEeSTkyUepa2nFU4eiBoytlUWWj5dblzRUSFfsqlo6FHRs5bnm8qVKyMr/y44noE/eqHKraqpWqS05iT6aefHbK71T/j/Qf62vEavJrPtUm1E7UedX11RvV1zdINBQ2wo0pjXOng08PnbE909Gk0VTVLNScfxacTTn7/KeQn8ZaXFp6z9HPNZ1XPF/RSm3Na4PatrUttEe1T3QEdgx3Onf2dpl2tf6s+XPtBdkL5RcFLxZ2E7tzujmXMi4t9rB65i9HXp7q3dL78ErAlbt9nn2DV12uXr9mf+1Kv1X/petm1y/cMLnReZN+s/2W4a22AYOB1l8MfmkdNBxsu210u2PIeKhreP1w94jFyOU7tneu3XW6e2t0w+jwmO/YvfHg8Yl7Yfdm78fdf/Ug9cHyw92PMI/yHvM9Lnki8aT6V9VfmycMJy5O2k4OPPV++nCKOfXit6TfVqZznlGelcxIz9TP6s5emLOfG3q+8fn0C9aL5fnc3/l/r3ip8vL8H5Z/DCwELEy/Yr/ivC54I/qm9q3+295Fj8Un7+LfLS/lvRd9X/eB/qH/o//HmeW0FdxK6SfVT12fXT4/4sRzOCwGm7FqBVBIwhERALyuBYASCAB1CPEPG9f8119+BvrK2fyNwVndL5jhvubRVsMQgCakeCFp04OsQ1LJEgAe5NodqT6WANbT+yf/iqQIPd21PXgaAcDJcjivtwJAQHLFgcNZ9uBwPlUgYhHf1z37f7V9g9e8ITewiP88wfWIYET6HPg21nzjV2fybQVcxfrg2/onng/F50lD/ccAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAAaNJREFUSA3FlT1LA0EQQBN/gYUYRTksJZVgEbCR/D+7QMr8ABtttBBCsLGzsLG2sxaxED/ie4d77u0dyaE5HHjczn7MzO7M7nU6/yXz+bwLhzCCjTQO+rZhDH3opuNLdRYN4RHe4RIKJ7R34Ro+4AEGSw2mE1iUwT18gpI74WvkGlccu4XNdH0jnYU7cAUacidn37qR23cOxc4aGU0nYUAn7iSWEHkz46w0ocdQu1X6B/AMQZ5o7KfBqNOfwRH8JB7FajGhnmcpKvQe3MEbvILiDm5gPXaCHnZr4vvFGMoEKudKn8YvQIOOe+YzCPop7dwJ3zRfJ7GDuso4YJGRa0yZgg4tUaNXdGrbuZWKKxzYYEJc2xp9AUUjGt8KC2jvgYadF8+10vJyDnNLXwbdiWUZi0fUK01Eoc+AZhCLZVzK4Vq6sDUdz+0dEcbbTTIOJmAyTVhx/WmvrExbv2jtPhWLKodjCtefZiEeZeVZWWSndgwj6fVf3XON8Qwq15++uoqrfYVrow6dGBpCq79ME291jaB0/Q2CPncyht/99MNO/vr9AqW/CGi8sJqbAAAAAElFTkSuQmCC")
|
||||
.previewLayout(.fixed(width: 63, height: 63))
|
||||
.background(.black)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ private let terminalFont = Font.custom("Menlo", size: 16)
|
||||
struct TerminalView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State var inProgress: Bool = false
|
||||
@State var message: String = ""
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@State var editing: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@@ -54,7 +56,9 @@ struct TerminalView: View {
|
||||
SendMessageView(
|
||||
sendMessage: sendMessage,
|
||||
inProgress: inProgress,
|
||||
keyboardVisible: $keyboardVisible
|
||||
message: $message,
|
||||
keyboardVisible: $keyboardVisible,
|
||||
editing: $editing
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,15 +30,18 @@ struct SettingsView: View {
|
||||
.navigationTitle("Your chat profile")
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "person.crop.circle")
|
||||
.padding(.trailing, 8)
|
||||
ProfileImage(imageStr: user.image)
|
||||
.frame(width: 44, height: 44)
|
||||
.padding(.trailing, 6)
|
||||
.padding(.vertical, 6)
|
||||
VStack(alignment: .leading) {
|
||||
Text(user.profile.displayName)
|
||||
Text(user.displayName)
|
||||
.fontWeight(.bold)
|
||||
.font(.title2)
|
||||
Text(user.profile.fullName)
|
||||
Text(user.fullName)
|
||||
}
|
||||
}
|
||||
.padding(.leading, -8)
|
||||
}
|
||||
NavigationLink {
|
||||
UserAddress()
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,3 +6,18 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#if defined(__x86_64__) && TARGET_IPHONE_SIMULATOR
|
||||
|
||||
#import <dirent.h>
|
||||
|
||||
int readdir_r$INODE64(DIR *restrict dirp, struct dirent *restrict entry,
|
||||
struct dirent **restrict result) {
|
||||
return readdir_r(dirp, entry, result);
|
||||
}
|
||||
|
||||
DIR *opendir$INODE64(const char *name) {
|
||||
return opendir(name);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -9,20 +9,20 @@
|
||||
/* 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 */; };
|
||||
5C1A4C1F27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
|
||||
5C27D01727E863F900DD6182 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D01227E863F800DD6182 /* libffi.a */; };
|
||||
5C27D01827E863F900DD6182 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D01227E863F800DD6182 /* libffi.a */; };
|
||||
5C27D01927E863F900DD6182 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D01327E863F800DD6182 /* libgmp.a */; };
|
||||
5C27D01A27E863F900DD6182 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D01327E863F800DD6182 /* libgmp.a */; };
|
||||
5C27D01B27E863F900DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D01427E863F800DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh.a */; };
|
||||
5C27D01C27E863F900DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D01427E863F800DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh.a */; };
|
||||
5C27D01D27E863F900DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D01527E863F900DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh-ghc8.10.7.a */; };
|
||||
5C27D01E27E863F900DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D01527E863F900DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh-ghc8.10.7.a */; };
|
||||
5C27D01F27E863F900DD6182 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D01627E863F900DD6182 /* libgmpxx.a */; };
|
||||
5C27D02027E863F900DD6182 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C27D01627E863F900DD6182 /* libgmpxx.a */; };
|
||||
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
|
||||
5C2E260827A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
|
||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; };
|
||||
@@ -43,6 +43,10 @@
|
||||
5C5346A927B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
|
||||
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; };
|
||||
5C577F7E27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; };
|
||||
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; };
|
||||
5C5F2B6E27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; };
|
||||
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; };
|
||||
5C5F2B7127EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; };
|
||||
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; };
|
||||
5C6AD81427A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; };
|
||||
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; };
|
||||
@@ -106,12 +110,12 @@
|
||||
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 */; };
|
||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
||||
64AA1C6A27EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -133,13 +137,13 @@
|
||||
|
||||
/* 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>"; };
|
||||
5C27D01227E863F800DD6182 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C27D01327E863F800DD6182 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C27D01427E863F800DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh.a"; sourceTree = "<group>"; };
|
||||
5C27D01527E863F900DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5C27D01627E863F900DD6182 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@@ -151,6 +155,8 @@
|
||||
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>"; };
|
||||
5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
|
||||
5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = "<group>"; };
|
||||
5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; };
|
||||
5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = "<group>"; };
|
||||
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
|
||||
@@ -190,9 +196,9 @@
|
||||
5CE4407127ADB1D0007B033A /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.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>"; };
|
||||
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -200,14 +206,14 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C27D01D27E863F900DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh-ghc8.10.7.a in Frameworks */,
|
||||
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */,
|
||||
5C0E5EFC27E24676003DE3D0 /* libgmp.a in Frameworks */,
|
||||
5C27D01927E863F900DD6182 /* 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 */,
|
||||
5C27D01F27E863F900DD6182 /* libgmpxx.a in Frameworks */,
|
||||
5C27D01B27E863F900DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh.a in Frameworks */,
|
||||
5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */,
|
||||
5C0E5EFA27E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj-ghc8.10.7.a in Frameworks */,
|
||||
5C27D01727E863F900DD6182 /* libffi.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -216,12 +222,12 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C764E85279C748C000C6508 /* libz.tbd in Frameworks */,
|
||||
5C0E5EF727E24676003DE3D0 /* libHSsimplex-chat-1.3.2-6OWqTXVUCEWLoNzSi0aRKj.a in Frameworks */,
|
||||
5C27D01C27E863F900DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh.a in Frameworks */,
|
||||
5C27D01827E863F900DD6182 /* libffi.a in Frameworks */,
|
||||
5C27D01A27E863F900DD6182 /* libgmp.a in Frameworks */,
|
||||
5C27D01E27E863F900DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh-ghc8.10.7.a in Frameworks */,
|
||||
5C764E84279C748C000C6508 /* libiconv.tbd 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 */,
|
||||
5C27D02027E863F900DD6182 /* libgmpxx.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -273,11 +279,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
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 */,
|
||||
5C27D01227E863F800DD6182 /* libffi.a */,
|
||||
5C27D01327E863F800DD6182 /* libgmp.a */,
|
||||
5C27D01627E863F900DD6182 /* libgmpxx.a */,
|
||||
5C27D01527E863F900DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh-ghc8.10.7.a */,
|
||||
5C27D01427E863F800DD6182 /* libHSsimplex-chat-1.3.3-ILT6I8E75GnANrGUR83jRh.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -310,6 +316,8 @@
|
||||
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */,
|
||||
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */,
|
||||
5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */,
|
||||
5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */,
|
||||
5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
@@ -432,7 +440,7 @@
|
||||
children = (
|
||||
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */,
|
||||
5CEACCE227DE9246000BD591 /* ComposeView.swift */,
|
||||
5CEACCE627DE97B6000BD591 /* QuotedItemView.swift */,
|
||||
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */,
|
||||
);
|
||||
path = ComposeMessage;
|
||||
sourceTree = "<group>";
|
||||
@@ -615,7 +623,6 @@
|
||||
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 */,
|
||||
@@ -634,6 +641,7 @@
|
||||
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */,
|
||||
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */,
|
||||
5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */,
|
||||
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */,
|
||||
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */,
|
||||
5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */,
|
||||
5CCD403727A5F9A200368C90 /* ConnectContactView.swift in Sources */,
|
||||
@@ -644,8 +652,10 @@
|
||||
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */,
|
||||
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
|
||||
5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */,
|
||||
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
|
||||
5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */,
|
||||
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */,
|
||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -664,7 +674,6 @@
|
||||
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 */,
|
||||
@@ -683,6 +692,7 @@
|
||||
5C2E261027A30FDC00F70299 /* ChatView.swift in Sources */,
|
||||
5C2E260C27A30CFA00F70299 /* ChatListView.swift in Sources */,
|
||||
5C971E2227AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */,
|
||||
5C5F2B6E27EBC3FE006A9D5F /* ImagePicker.swift in Sources */,
|
||||
5C577F7E27C83AA10006112D /* MarkdownHelp.swift in Sources */,
|
||||
5CA059EC279559F40002BEB4 /* SimpleXApp.swift in Sources */,
|
||||
5CCD403827A5F9A200368C90 /* ConnectContactView.swift in Sources */,
|
||||
@@ -693,8 +703,10 @@
|
||||
5CC1C99627A6CF7F000D9FF6 /* ShareSheet.swift in Sources */,
|
||||
5C2E260827A2941F00F70299 /* SimpleXAPI.swift in Sources */,
|
||||
5CB924D527A853F100ACCCDD /* SettingsButton.swift in Sources */,
|
||||
5C5F2B7127EBC704006A9D5F /* ProfileImage.swift in Sources */,
|
||||
5CE4407327ADB1D0007B033A /* Emoji.swift in Sources */,
|
||||
5C1A4C1F27A715B700EAD5AD /* ChatItemView.swift in Sources */,
|
||||
64AA1C6A27EE10C800AC7277 /* ContextItemView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -851,7 +863,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 29;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -871,7 +883,7 @@
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios";
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim";
|
||||
MARKETING_VERSION = 1.3;
|
||||
MARKETING_VERSION = 1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -891,7 +903,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 29;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -909,9 +921,10 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "";
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios";
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim";
|
||||
MARKETING_VERSION = 1.3;
|
||||
MARKETING_VERSION = 1.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -3,7 +3,7 @@ packages: .
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 5c6ec96d6477371d8e617bcc71e6ecbcdd5c78cc
|
||||
tag: 800581b2bf5dacb2134dfda751be08cbf78df978
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
24
flake.lock
generated
24
flake.lock
generated
@@ -133,11 +133,11 @@
|
||||
"hackage": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1646625282,
|
||||
"narHash": "sha256-U23F/EXZC1UOxO3SkfzS82TwYtT42sp5Y6BImXsHWMo=",
|
||||
"lastModified": 1647047557,
|
||||
"narHash": "sha256-6A7jjz77f53GkvFxqVmeuqqXyDWsU24rUtFtOg68CAg=",
|
||||
"owner": "input-output-hk",
|
||||
"repo": "hackage.nix",
|
||||
"rev": "bff4ab542bc6f68fc078ccd0df2e8eae61650e32",
|
||||
"rev": "fc07d4d4f2597334caa96f455cec190bdcc931f4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -169,11 +169,11 @@
|
||||
"stackage": "stackage"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1646643560,
|
||||
"narHash": "sha256-mCzOavKLzXof7NuTBGQx+KWX2AIarrxxGykBE4OvjzY=",
|
||||
"lastModified": 1647308139,
|
||||
"narHash": "sha256-GRvEGSCz9YQwE/zYUtFYkq2mNm1QxVNyfVwfN+o6mbM=",
|
||||
"owner": "input-output-hk",
|
||||
"repo": "haskell.nix",
|
||||
"rev": "98de1769b4d5d9a4d137a77c5ec153c900ab0fa5",
|
||||
"rev": "d42e6bdd52b6a36ee54344a0d680ce248e64773f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -217,11 +217,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1645623357,
|
||||
"narHash": "sha256-vAaI91QFn/kY/uMiebW+kG2mPmxirMSJWYtkqkBKdDc=",
|
||||
"lastModified": 1646955661,
|
||||
"narHash": "sha256-AYLta1PubJnrkv15+7G+6ErW5m9NcI9wSdJ+n7pKAe0=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9222ae36b208d1c6b55d88e10aa68f969b5b5244",
|
||||
"rev": "e9545762b032559c27d8ec9141ed63ceca1aa1ac",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -322,11 +322,11 @@
|
||||
"stackage": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1646625386,
|
||||
"narHash": "sha256-dIsnm5vx9Dlxx/rRjFyO7uMBfKjEN6RX7oAenwfetHY=",
|
||||
"lastModified": 1646961451,
|
||||
"narHash": "sha256-fs3+CsqzgNVT2mJSJOc+MnhbRoIoB/L1ZEhiJn0nXHQ=",
|
||||
"owner": "input-output-hk",
|
||||
"repo": "stackage.nix",
|
||||
"rev": "e6a7664a79ed4ec8a19d76fb60731190b8763874",
|
||||
"rev": "02b9e7ea7304027b5d473233c2465d04a21a17e3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 1.3.3
|
||||
version: 1.4.0
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
@@ -61,6 +61,8 @@ tests:
|
||||
- hspec == 2.7.*
|
||||
- network == 3.1.*
|
||||
- stm == 2.5.*
|
||||
ghc-options:
|
||||
- -threaded
|
||||
|
||||
ghc-options:
|
||||
# - -haddock
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"git://github.com/simplex-chat/simplexmq.git"."5c6ec96d6477371d8e617bcc71e6ecbcdd5c78cc" = "0qjmldlrxl5waqfbsckjhxkd3zn25bkbyqwf9l0r4gq3c7l6k358";
|
||||
"git://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
|
||||
"git://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
|
||||
"git://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."800581b2bf5dacb2134dfda751be08cbf78df978" = "1xmn6dfwmmc84zpj9pnklxc4lh4bwwf6pv55qaqcj15crvqhvnyg";
|
||||
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
|
||||
"https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
|
||||
"https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97";
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 1.3.3
|
||||
version: 1.4.0
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
@@ -32,6 +32,7 @@ library
|
||||
Simplex.Chat.Migrations.M20220301_smp_servers
|
||||
Simplex.Chat.Migrations.M20220302_profile_images
|
||||
Simplex.Chat.Migrations.M20220304_msg_quotes
|
||||
Simplex.Chat.Migrations.M20220321_chat_item_edited
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Options
|
||||
Simplex.Chat.Protocol
|
||||
|
||||
@@ -215,6 +215,38 @@ processChatCommand = \case
|
||||
msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Just memberId}
|
||||
in sendNewGroupMsg user group (MCQuote QuotedMsg {msgRef, content} mc) mc (Just quotedItem)
|
||||
CTContactRequest -> pure $ chatCmdError "not supported"
|
||||
APIUpdateMessage cType chatId itemId mc -> withUser $ \user@User {userId} -> withChatLock $ case cType of
|
||||
CTDirect -> do
|
||||
(ct@Contact {contactId, localDisplayName = c}, ci) <- withStore $ \st -> (,) <$> getContact st userId chatId <*> getDirectChatItem st userId chatId itemId
|
||||
case ci of
|
||||
CChatItem SMDSnd ChatItem {meta = CIMeta {itemSharedMsgId}, content = ciContent} -> do
|
||||
case (ciContent, itemSharedMsgId) of
|
||||
(CISndMsgContent _, Just itemSharedMId) -> do
|
||||
SndMessage {msgId} <- sendDirectContactMessage ct (XMsgUpdate itemSharedMId mc)
|
||||
updCi <- withStore $ \st -> updateDirectChatItem st userId contactId itemId (CISndMsgContent mc) msgId
|
||||
setActive $ ActiveC c
|
||||
pure . CRChatItemUpdated $ AChatItem SCTDirect SMDSnd (DirectChat ct) updCi
|
||||
_ -> throwChatError CEInvalidMessageUpdate
|
||||
CChatItem SMDRcv _ -> throwChatError CEInvalidMessageUpdate
|
||||
CTGroup -> do
|
||||
Group gInfo@GroupInfo {groupId, localDisplayName = gName, membership} ms <- withStore $ \st -> getGroup st user chatId
|
||||
unless (memberActive membership) $ throwChatError CEGroupMemberUserRemoved
|
||||
ci <- withStore $ \st -> getGroupChatItem st user chatId itemId
|
||||
case ci of
|
||||
CChatItem SMDSnd ChatItem {meta = CIMeta {itemSharedMsgId}, content = ciContent} -> do
|
||||
case (ciContent, itemSharedMsgId) of
|
||||
(CISndMsgContent _, Just itemSharedMId) -> do
|
||||
SndMessage {msgId} <- sendGroupMessage gInfo ms (XMsgUpdate itemSharedMId mc)
|
||||
updCi <- withStore $ \st -> updateGroupChatItem st user groupId itemId (CISndMsgContent mc) msgId
|
||||
setActive $ ActiveG gName
|
||||
pure . CRChatItemUpdated $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) updCi
|
||||
_ -> throwChatError CEInvalidMessageUpdate
|
||||
CChatItem SMDRcv _ -> throwChatError CEInvalidMessageUpdate
|
||||
CTContactRequest -> pure $ chatCmdError "not supported"
|
||||
APIDeleteMessage cType _chatId _itemId _mode -> withUser $ \_user -> withChatLock $ case cType of
|
||||
CTDirect -> pure CRCmdOk
|
||||
CTGroup -> pure CRCmdOk
|
||||
CTContactRequest -> pure $ chatCmdError "not supported"
|
||||
APIChatRead cType chatId fromToIds -> withChatLock $ case cType of
|
||||
CTDirect -> withStore (\st -> updateDirectChatItemsRead st chatId fromToIds) $> CRCmdOk
|
||||
CTGroup -> withStore (\st -> updateGroupChatItemsRead st chatId fromToIds) $> CRCmdOk
|
||||
@@ -244,6 +276,7 @@ processChatCommand = \case
|
||||
`E.finally` deleteContactRequest st userId connReqId
|
||||
withAgent $ \a -> rejectContact a connId invId
|
||||
pure $ CRContactRequestRejected cReq
|
||||
APIUpdateProfile profile -> withUser (`updateProfile` profile)
|
||||
GetUserSMPServers -> CRUserSMPServers <$> withUser (\user -> withStore (`getSMPServers` user))
|
||||
SetUserSMPServers smpServers -> withUser $ \user -> withChatLock $ do
|
||||
withStore $ \st -> overwriteSMPServers st user smpServers
|
||||
@@ -381,8 +414,8 @@ processChatCommand = \case
|
||||
SendFile cName f -> withUser $ \user@User {userId} -> withChatLock $ do
|
||||
(fileSize, chSize) <- checkSndFile f
|
||||
contact <- withStore $ \st -> getContactByName st userId cName
|
||||
(agentConnId, fileConnReq) <- withAgent (`createConnection` SCMInvitation)
|
||||
let fileInv = FileInvitation {fileName = takeFileName f, fileSize, fileConnReq}
|
||||
(agentConnId, connReq) <- withAgent (`createConnection` SCMInvitation)
|
||||
let fileInv = FileInvitation {fileName = takeFileName f, fileSize, fileConnReq = ACR SCMInvitation connReq}
|
||||
SndFileTransfer {fileId} <- withStore $ \st ->
|
||||
createSndFileTransfer st userId contact f fileInv agentConnId chSize
|
||||
ci <- sendDirectChatItem user contact (XFile fileInv) (CISndFileInvitation fileId f) Nothing
|
||||
@@ -395,8 +428,8 @@ processChatCommand = \case
|
||||
unless (memberActive membership) $ throwChatError CEGroupMemberUserRemoved
|
||||
let fileName = takeFileName f
|
||||
ms <- forM (filter memberActive members) $ \m -> do
|
||||
(connId, fileConnReq) <- withAgent (`createConnection` SCMInvitation)
|
||||
pure (m, connId, FileInvitation {fileName, fileSize, fileConnReq})
|
||||
(connId, connReq) <- withAgent (`createConnection` SCMInvitation)
|
||||
pure (m, connId, FileInvitation {fileName, fileSize, fileConnReq = ACR SCMInvitation connReq})
|
||||
fileId <- withStore $ \st -> createSndGroupFileTransfer st userId gInfo ms f fileSize chSize
|
||||
-- TODO sendGroupChatItem - same file invitation to all
|
||||
forM_ ms $ \(m, _, fileInv) ->
|
||||
@@ -409,7 +442,7 @@ processChatCommand = \case
|
||||
withStore $ \st -> updateFileTransferChatItemId st fileId itemId
|
||||
pure . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) cItem
|
||||
ReceiveFile fileId filePath_ -> withUser $ \User {userId} -> do
|
||||
ft@RcvFileTransfer {fileInvitation = FileInvitation {fileName, fileConnReq}, fileStatus} <- withStore $ \st -> getRcvFileTransfer st userId fileId
|
||||
ft@RcvFileTransfer {fileInvitation = FileInvitation {fileName, fileConnReq = ACR _ fileConnReq}, fileStatus} <- withStore $ \st -> getRcvFileTransfer st userId fileId
|
||||
unless (fileStatus == RFSNew) . throwChatError $ CEFileAlreadyReceiving fileName
|
||||
withChatLock . procCmd $ do
|
||||
tryError (withAgent $ \a -> joinConnection a fileConnReq . directMessage $ XFileAcpt fileName) >>= \case
|
||||
@@ -670,10 +703,10 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
withAckMessage agentConnId meta $ pure ()
|
||||
ackMsgDeliveryEvent conn meta
|
||||
SENT msgId ->
|
||||
-- ? updateDirectChatItem
|
||||
-- ? updateDirectChatItemStatus
|
||||
sentMsgDeliveryEvent conn msgId
|
||||
-- TODO print errors
|
||||
MERR _ _ -> pure () -- ? updateDirectChatItem
|
||||
MERR _ _ -> pure () -- ? updateDirectChatItemStatus
|
||||
ERR _ -> pure ()
|
||||
-- TODO add debugging output
|
||||
_ -> pure ()
|
||||
@@ -683,6 +716,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
withAckMessage agentConnId msgMeta $
|
||||
case chatMsgEvent of
|
||||
XMsgNew mc -> newContentMessage ct mc msg msgMeta
|
||||
XMsgUpdate sharedMsgId mContent -> messageUpdate ct sharedMsgId mContent msg msgMeta
|
||||
XFile fInv -> processFileInvitation ct fInv msg msgMeta
|
||||
XInfo p -> xInfo ct p
|
||||
XGrpInv gInv -> processGroupInvitation ct gInv
|
||||
@@ -728,8 +762,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
case chatItemId_ of
|
||||
Nothing -> pure ()
|
||||
Just chatItemId -> do
|
||||
chatItem <- withStore $ \st -> updateDirectChatItem st userId contactId chatItemId CISSndSent
|
||||
toView $ CRChatItemUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem)
|
||||
chatItem <- withStore $ \st -> updateDirectChatItemStatus st userId contactId chatItemId CISSndSent
|
||||
toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem)
|
||||
END -> do
|
||||
toView $ CRContactAnotherClient ct
|
||||
showToast (c <> "> ") "connected to another client"
|
||||
@@ -747,8 +781,8 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
case chatItemId_ of
|
||||
Nothing -> pure ()
|
||||
Just chatItemId -> do
|
||||
chatItem <- withStore $ \st -> updateDirectChatItem st userId contactId chatItemId (agentErrToItemStatus err)
|
||||
toView $ CRChatItemUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem)
|
||||
chatItem <- withStore $ \st -> updateDirectChatItemStatus st userId contactId chatItemId (agentErrToItemStatus err)
|
||||
toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem)
|
||||
ERR _ -> pure ()
|
||||
-- TODO add debugging output
|
||||
_ -> pure ()
|
||||
@@ -822,6 +856,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
withAckMessage agentConnId msgMeta $
|
||||
case chatMsgEvent of
|
||||
XMsgNew mc -> newGroupContentMessage gInfo m mc msg msgMeta
|
||||
XMsgUpdate sharedMsgId mContent -> groupMessageUpdate gInfo sharedMsgId mContent msg
|
||||
XFile fInv -> processGroupFileInvitation gInfo m fInv msg msgMeta
|
||||
XGrpMemNew memInfo -> xGrpMemNew gInfo m memInfo
|
||||
XGrpMemIntro memInfo -> xGrpMemIntro conn gInfo m memInfo
|
||||
@@ -1000,6 +1035,13 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
showMsgToast (c <> "> ") content formattedText
|
||||
setActive $ ActiveC c
|
||||
|
||||
messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> m ()
|
||||
messageUpdate ct@Contact {contactId, localDisplayName = c} sharedMsgId mc RcvMessage {msgId} msgMeta = do
|
||||
updCi <- withStore $ \st -> updateDirectChatItemByMsgId st userId contactId sharedMsgId (CIRcvMsgContent mc) msgId
|
||||
toView . CRChatItemUpdated $ AChatItem SCTDirect SMDRcv (DirectChat ct) updCi
|
||||
checkIntegrity msgMeta $ toView . CRMsgIntegrityError
|
||||
setActive $ ActiveC c
|
||||
|
||||
newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> MsgMeta -> m ()
|
||||
newGroupContentMessage gInfo m@GroupMember {localDisplayName = c} mc msg msgMeta = do
|
||||
let content = mcContent mc
|
||||
@@ -1009,6 +1051,13 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
showMsgToast ("#" <> g <> " " <> c <> "> ") content formattedText
|
||||
setActive $ ActiveG g
|
||||
|
||||
groupMessageUpdate :: GroupInfo -> SharedMsgId -> MsgContent -> RcvMessage -> m ()
|
||||
groupMessageUpdate gInfo@GroupInfo {groupId} sharedMsgId mc RcvMessage {msgId} = do
|
||||
updCi <- withStore $ \st -> updateGroupChatItemByMsgId st user groupId sharedMsgId (CIRcvMsgContent mc) msgId
|
||||
toView . CRChatItemUpdated $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) updCi
|
||||
let g = groupName' gInfo
|
||||
setActive $ ActiveG g
|
||||
|
||||
processFileInvitation :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> m ()
|
||||
processFileInvitation ct@Contact {localDisplayName = c} fInv msg msgMeta = do
|
||||
-- TODO chunk size has to be sent as part of invitation
|
||||
@@ -1396,8 +1445,9 @@ saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} MsgMeta {broker = (_, brok
|
||||
mkChatItem :: MsgDirectionI d => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIQuote c) -> Maybe SharedMsgId -> ChatItemTs -> UTCTime -> IO (ChatItem c d)
|
||||
mkChatItem cd ciId content quotedItem sharedMsgId itemTs createdAt = do
|
||||
tz <- getCurrentTimeZone
|
||||
currentTs <- liftIO getCurrentTime
|
||||
let itemText = ciContentToText content
|
||||
meta = mkCIMeta ciId itemText ciStatusNew sharedMsgId tz itemTs createdAt
|
||||
meta = mkCIMeta ciId itemText ciStatusNew sharedMsgId False False tz currentTs itemTs createdAt
|
||||
pure ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem}
|
||||
|
||||
allowAgentConnection :: ChatMonad m => Connection -> ConfirmationId -> ChatMsgEvent -> m ()
|
||||
@@ -1517,10 +1567,13 @@ chatCommandP =
|
||||
<|> "/_get items count=" *> (APIGetChatItems <$> A.decimal)
|
||||
<|> "/_send " *> (APISendMessage <$> chatTypeP <*> A.decimal <* A.space <*> msgContentP)
|
||||
<|> "/_send_quote " *> (APISendMessageQuote <$> chatTypeP <*> A.decimal <* A.space <*> A.decimal <* A.space <*> msgContentP)
|
||||
<|> "/_update item " *> (APIUpdateMessage <$> chatTypeP <*> A.decimal <* A.space <*> A.decimal <* A.space <*> msgContentP)
|
||||
<|> "/_delete item " *> (APIDeleteMessage <$> chatTypeP <*> A.decimal <* A.space <*> A.decimal <* A.space <*> msgDeleteMode)
|
||||
<|> "/_read chat " *> (APIChatRead <$> chatTypeP <*> A.decimal <* A.space <*> ((,) <$> ("from=" *> A.decimal) <* A.space <*> ("to=" *> A.decimal)))
|
||||
<|> "/_delete " *> (APIDeleteChat <$> chatTypeP <*> A.decimal)
|
||||
<|> "/_accept " *> (APIAcceptContact <$> A.decimal)
|
||||
<|> "/_reject " *> (APIRejectContact <$> A.decimal)
|
||||
<|> "/_profile " *> (APIUpdateProfile <$> jsonP)
|
||||
<|> "/smp_servers default" $> SetUserSMPServers []
|
||||
<|> "/smp_servers " *> (SetUserSMPServers <$> smpServersP)
|
||||
<|> "/smp_servers" $> GetUserSMPServers
|
||||
@@ -1577,7 +1630,8 @@ chatCommandP =
|
||||
<|> (CPBefore <$ "before=" <*> A.decimal <* A.space <* "count=" <*> A.decimal)
|
||||
msgContentP =
|
||||
"text " *> (MCText . safeDecodeUtf8 <$> A.takeByteString)
|
||||
<|> "json " *> (J.eitherDecodeStrict' <$?> A.takeByteString)
|
||||
<|> "json " *> jsonP
|
||||
msgDeleteMode = "broadcast" $> MDBroadcast <|> "internal" $> MDInternal
|
||||
displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' '))
|
||||
sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A.space <*> pure msgDir <*> quotedMsg <*> A.takeByteString
|
||||
quotedMsg = A.char '(' *> A.takeTill (== ')') <* A.char ')' <* optional A.space
|
||||
@@ -1590,6 +1644,8 @@ chatCommandP =
|
||||
userProfile = do
|
||||
(cName, fullName) <- userNames
|
||||
pure Profile {displayName = cName, fullName, image = Nothing}
|
||||
jsonP :: J.FromJSON a => Parser a
|
||||
jsonP = J.eitherDecodeStrict' <$?> A.takeByteString
|
||||
groupProfile = do
|
||||
gName <- displayName
|
||||
fullName <- fullNameP gName
|
||||
|
||||
@@ -81,6 +81,9 @@ data ChatController = ChatController
|
||||
data HelpSection = HSMain | HSFiles | HSGroups | HSMyAddress | HSMarkdown | HSQuotes
|
||||
deriving (Show, Generic)
|
||||
|
||||
data MsgDeleteMode = MDBroadcast | MDInternal
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON HelpSection where
|
||||
toJSON = J.genericToJSON . enumJSON $ dropPrefix "HS"
|
||||
toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "HS"
|
||||
@@ -94,10 +97,13 @@ data ChatCommand
|
||||
| APIGetChatItems Int
|
||||
| APISendMessage ChatType Int64 MsgContent
|
||||
| APISendMessageQuote ChatType Int64 ChatItemId MsgContent
|
||||
| APIUpdateMessage ChatType Int64 ChatItemId MsgContent
|
||||
| APIDeleteMessage ChatType Int64 ChatItemId MsgDeleteMode
|
||||
| APIChatRead ChatType Int64 (ChatItemId, ChatItemId)
|
||||
| APIDeleteChat ChatType Int64
|
||||
| APIAcceptContact Int64
|
||||
| APIRejectContact Int64
|
||||
| APIUpdateProfile Profile
|
||||
| GetUserSMPServers
|
||||
| SetUserSMPServers [SMPServer]
|
||||
| ChatHelp HelpSection
|
||||
@@ -146,7 +152,9 @@ data ChatResponse
|
||||
| CRApiChat {chat :: AChat}
|
||||
| CRUserSMPServers {smpServers :: [SMPServer]}
|
||||
| CRNewChatItem {chatItem :: AChatItem}
|
||||
| CRChatItemStatusUpdated {chatItem :: AChatItem}
|
||||
| CRChatItemUpdated {chatItem :: AChatItem}
|
||||
| CRChatItemDeleted {chatItem :: AChatItem}
|
||||
| CRMsgIntegrityError {msgerror :: MsgErrorType} -- TODO make it chat item to support in mobile
|
||||
| CRCmdAccepted {corr :: CorrId}
|
||||
| CRCmdOk
|
||||
@@ -295,6 +303,7 @@ data ChatErrorType
|
||||
| CEFileRcvChunk {message :: String}
|
||||
| CEFileInternal {message :: String}
|
||||
| CEInvalidQuote
|
||||
| CEInvalidMessageUpdate
|
||||
| CEAgentVersion
|
||||
| CECommandError {message :: String}
|
||||
deriving (Show, Exception, Generic)
|
||||
|
||||
@@ -22,7 +22,7 @@ import Data.Int (Int64)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Time.Clock (UTCTime, diffUTCTime, nominalDay)
|
||||
import Data.Time.LocalTime (TimeZone, ZonedTime, utcToZonedTime)
|
||||
import Data.Type.Equality
|
||||
import Data.Typeable (Typeable)
|
||||
@@ -206,15 +206,19 @@ data CIMeta (d :: MsgDirection) = CIMeta
|
||||
itemText :: Text,
|
||||
itemStatus :: CIStatus d,
|
||||
itemSharedMsgId :: Maybe SharedMsgId,
|
||||
itemDeleted :: Bool,
|
||||
itemEdited :: Bool,
|
||||
editable :: Bool,
|
||||
localItemTs :: ZonedTime,
|
||||
createdAt :: UTCTime
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
mkCIMeta :: ChatItemId -> Text -> CIStatus d -> Maybe SharedMsgId -> TimeZone -> ChatItemTs -> UTCTime -> CIMeta d
|
||||
mkCIMeta itemId itemText itemStatus itemSharedMsgId tz itemTs createdAt =
|
||||
mkCIMeta :: ChatItemId -> Text -> CIStatus d -> Maybe SharedMsgId -> Bool -> Bool -> TimeZone -> UTCTime -> ChatItemTs -> UTCTime -> CIMeta d
|
||||
mkCIMeta itemId itemText itemStatus itemSharedMsgId itemDeleted itemEdited tz currentTs itemTs createdAt =
|
||||
let localItemTs = utcToZonedTime tz itemTs
|
||||
in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, localItemTs, createdAt}
|
||||
editable = diffUTCTime currentTs itemTs < nominalDay
|
||||
in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemDeleted, itemEdited, editable, localItemTs, createdAt}
|
||||
|
||||
instance ToJSON (CIMeta d) where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
@@ -343,6 +347,8 @@ type ChatItemTs = UTCTime
|
||||
data CIContent (d :: MsgDirection) where
|
||||
CISndMsgContent :: MsgContent -> CIContent 'MDSnd
|
||||
CIRcvMsgContent :: MsgContent -> CIContent 'MDRcv
|
||||
CISndMsgDeleted :: MsgContent -> CIContent 'MDSnd
|
||||
CIRcvMsgDeleted :: MsgContent -> CIContent 'MDRcv
|
||||
CISndFileInvitation :: FileTransferId -> FilePath -> CIContent 'MDSnd
|
||||
CIRcvFileInvitation :: RcvFileTransfer -> CIContent 'MDRcv
|
||||
|
||||
@@ -352,6 +358,8 @@ ciContentToText :: CIContent d -> Text
|
||||
ciContentToText = \case
|
||||
CISndMsgContent mc -> msgContentText mc
|
||||
CIRcvMsgContent mc -> msgContentText mc
|
||||
CISndMsgDeleted _ -> "this message is deleted"
|
||||
CIRcvMsgDeleted _ -> "this message is deleted"
|
||||
CISndFileInvitation fId fPath -> "you sent file #" <> T.pack (show fId) <> ": " <> T.pack fPath
|
||||
CIRcvFileInvitation RcvFileTransfer {fileInvitation = FileInvitation {fileName}} -> "file " <> T.pack fileName
|
||||
|
||||
@@ -379,6 +387,8 @@ instance FromField ACIContent where fromField = fromTextField_ $ fmap aciContent
|
||||
data JSONCIContent
|
||||
= JCISndMsgContent {msgContent :: MsgContent}
|
||||
| JCIRcvMsgContent {msgContent :: MsgContent}
|
||||
| JCISndMsgDeleted {msgContent :: MsgContent}
|
||||
| JCIRcvMsgDeleted {msgContent :: MsgContent}
|
||||
| JCISndFileInvitation {fileId :: FileTransferId, filePath :: FilePath}
|
||||
| JCIRcvFileInvitation {rcvFileTransfer :: RcvFileTransfer}
|
||||
deriving (Generic)
|
||||
@@ -394,6 +404,8 @@ jsonCIContent :: CIContent d -> JSONCIContent
|
||||
jsonCIContent = \case
|
||||
CISndMsgContent mc -> JCISndMsgContent mc
|
||||
CIRcvMsgContent mc -> JCIRcvMsgContent mc
|
||||
CISndMsgDeleted mc -> JCISndMsgDeleted mc
|
||||
CIRcvMsgDeleted mc -> JCIRcvMsgDeleted mc
|
||||
CISndFileInvitation fId fPath -> JCISndFileInvitation fId fPath
|
||||
CIRcvFileInvitation ft -> JCIRcvFileInvitation ft
|
||||
|
||||
@@ -401,6 +413,8 @@ aciContentJSON :: JSONCIContent -> ACIContent
|
||||
aciContentJSON = \case
|
||||
JCISndMsgContent mc -> ACIContent SMDSnd $ CISndMsgContent mc
|
||||
JCIRcvMsgContent mc -> ACIContent SMDRcv $ CIRcvMsgContent mc
|
||||
JCISndMsgDeleted mc -> ACIContent SMDSnd $ CISndMsgDeleted mc
|
||||
JCIRcvMsgDeleted mc -> ACIContent SMDRcv $ CIRcvMsgDeleted mc
|
||||
JCISndFileInvitation fId fPath -> ACIContent SMDSnd $ CISndFileInvitation fId fPath
|
||||
JCIRcvFileInvitation ft -> ACIContent SMDRcv $ CIRcvFileInvitation ft
|
||||
|
||||
@@ -408,6 +422,8 @@ aciContentJSON = \case
|
||||
data DBJSONCIContent
|
||||
= DBJCISndMsgContent {msgContent :: MsgContent}
|
||||
| DBJCIRcvMsgContent {msgContent :: MsgContent}
|
||||
| DBJCISndMsgDeleted {msgContent :: MsgContent}
|
||||
| DBJCIRcvMsgDeleted {msgContent :: MsgContent}
|
||||
| DBJCISndFileInvitation {fileId :: FileTransferId, filePath :: FilePath}
|
||||
| DBJCIRcvFileInvitation {rcvFileTransfer :: RcvFileTransfer}
|
||||
deriving (Generic)
|
||||
@@ -423,6 +439,8 @@ dbJsonCIContent :: CIContent d -> DBJSONCIContent
|
||||
dbJsonCIContent = \case
|
||||
CISndMsgContent mc -> DBJCISndMsgContent mc
|
||||
CIRcvMsgContent mc -> DBJCIRcvMsgContent mc
|
||||
CISndMsgDeleted mc -> DBJCISndMsgDeleted mc
|
||||
CIRcvMsgDeleted mc -> DBJCIRcvMsgDeleted mc
|
||||
CISndFileInvitation fId fPath -> DBJCISndFileInvitation fId fPath
|
||||
CIRcvFileInvitation ft -> DBJCIRcvFileInvitation ft
|
||||
|
||||
@@ -430,6 +448,8 @@ aciContentDBJSON :: DBJSONCIContent -> ACIContent
|
||||
aciContentDBJSON = \case
|
||||
DBJCISndMsgContent mc -> ACIContent SMDSnd $ CISndMsgContent mc
|
||||
DBJCIRcvMsgContent mc -> ACIContent SMDRcv $ CIRcvMsgContent mc
|
||||
DBJCISndMsgDeleted ciId -> ACIContent SMDSnd $ CISndMsgDeleted ciId
|
||||
DBJCIRcvMsgDeleted ciId -> ACIContent SMDRcv $ CIRcvMsgDeleted ciId
|
||||
DBJCISndFileInvitation fId fPath -> ACIContent SMDSnd $ CISndFileInvitation fId fPath
|
||||
DBJCIRcvFileInvitation ft -> ACIContent SMDRcv $ CIRcvFileInvitation ft
|
||||
|
||||
|
||||
12
src/Simplex/Chat/Migrations/M20220321_chat_item_edited.hs
Normal file
12
src/Simplex/Chat/Migrations/M20220321_chat_item_edited.hs
Normal file
@@ -0,0 +1,12 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20220321_chat_item_edited where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20220321_chat_item_edited :: Query
|
||||
m20220321_chat_item_edited =
|
||||
[sql|
|
||||
ALTER TABLE chat_items ADD COLUMN item_edited INTEGER; -- 1 for edited
|
||||
|]
|
||||
@@ -109,6 +109,8 @@ instance StrEncoding ChatMessage where
|
||||
|
||||
data ChatMsgEvent
|
||||
= XMsgNew MsgContainer
|
||||
| XMsgUpdate SharedMsgId MsgContent
|
||||
| XMsgDel SharedMsgId
|
||||
| XFile FileInvitation
|
||||
| XFileAcpt String
|
||||
| XInfo Profile
|
||||
@@ -232,6 +234,8 @@ instance FromField MsgContent where
|
||||
|
||||
data CMEventTag
|
||||
= XMsgNew_
|
||||
| XMsgUpdate_
|
||||
| XMsgDel_
|
||||
| XFile_
|
||||
| XFileAcpt_
|
||||
| XInfo_
|
||||
@@ -258,6 +262,8 @@ data CMEventTag
|
||||
instance StrEncoding CMEventTag where
|
||||
strEncode = \case
|
||||
XMsgNew_ -> "x.msg.new"
|
||||
XMsgUpdate_ -> "x.msg.update"
|
||||
XMsgDel_ -> "x.msg.del"
|
||||
XFile_ -> "x.file"
|
||||
XFileAcpt_ -> "x.file.acpt"
|
||||
XInfo_ -> "x.info"
|
||||
@@ -281,6 +287,8 @@ instance StrEncoding CMEventTag where
|
||||
XUnknown_ t -> encodeUtf8 t
|
||||
strDecode = \case
|
||||
"x.msg.new" -> Right XMsgNew_
|
||||
"x.msg.update" -> Right XMsgUpdate_
|
||||
"x.msg.del" -> Right XMsgDel_
|
||||
"x.file" -> Right XFile_
|
||||
"x.file.acpt" -> Right XFileAcpt_
|
||||
"x.info" -> Right XInfo_
|
||||
@@ -307,6 +315,8 @@ instance StrEncoding CMEventTag where
|
||||
toCMEventTag :: ChatMsgEvent -> CMEventTag
|
||||
toCMEventTag = \case
|
||||
XMsgNew _ -> XMsgNew_
|
||||
XMsgUpdate _ _ -> XMsgUpdate_
|
||||
XMsgDel _ -> XMsgDel_
|
||||
XFile _ -> XFile_
|
||||
XFileAcpt _ -> XFileAcpt_
|
||||
XInfo _ -> XInfo_
|
||||
@@ -350,7 +360,9 @@ appToChatMessage AppMessage {msgId, event, params} = do
|
||||
opt :: FromJSON a => J.Key -> Either String (Maybe a)
|
||||
opt key = JT.parseEither (.:? key) params
|
||||
msg = \case
|
||||
XMsgNew_ -> XMsgNew <$> JT.parseEither parseMsgContainer params
|
||||
XMsgNew_ -> XMsgNew <$> JT.parseEither parseMsgContainer params
|
||||
XMsgUpdate_ -> XMsgUpdate <$> p "msgId" <*> p "content"
|
||||
XMsgDel_ -> XMsgDel <$> p "msgId"
|
||||
XFile_ -> XFile <$> p "file"
|
||||
XFileAcpt_ -> XFileAcpt <$> p "fileName"
|
||||
XInfo_ -> XInfo <$> p "profile"
|
||||
@@ -382,6 +394,8 @@ chatToAppMessage ChatMessage {msgId, chatMsgEvent} = AppMessage {msgId, event, p
|
||||
key .=? value = maybe id ((:) . (key .=)) value
|
||||
params = case chatMsgEvent of
|
||||
XMsgNew container -> msgContainerJSON container
|
||||
XMsgUpdate msgId' content -> o ["msgId" .= msgId', "content" .= content]
|
||||
XMsgDel msgId' -> o ["msgId" .= msgId']
|
||||
XFile fileInv -> o ["file" .= fileInv]
|
||||
XFileAcpt fileName -> o ["fileName" .= fileName]
|
||||
XInfo profile -> o ["profile" .= profile]
|
||||
|
||||
@@ -123,8 +123,12 @@ module Simplex.Chat.Store
|
||||
getGroupChatItem,
|
||||
getDirectChatItemIdByText,
|
||||
getGroupChatItemIdByText,
|
||||
updateDirectChatItemStatus,
|
||||
updateDirectChatItem,
|
||||
updateDirectChatItemByMsgId,
|
||||
updateDirectChatItemsRead,
|
||||
updateGroupChatItem,
|
||||
updateGroupChatItemByMsgId,
|
||||
updateGroupChatItemsRead,
|
||||
getSMPServers,
|
||||
overwriteSMPServers,
|
||||
@@ -148,7 +152,7 @@ import Data.Function (on)
|
||||
import Data.Functor (($>))
|
||||
import Data.Int (Int64)
|
||||
import Data.List (find, sortBy, sortOn)
|
||||
import Data.Maybe (isJust, listToMaybe)
|
||||
import Data.Maybe (fromMaybe, isJust, listToMaybe)
|
||||
import Data.Ord (Down (..))
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
@@ -168,10 +172,11 @@ import Simplex.Chat.Migrations.M20220224_messages_fks
|
||||
import Simplex.Chat.Migrations.M20220301_smp_servers
|
||||
import Simplex.Chat.Migrations.M20220302_profile_images
|
||||
import Simplex.Chat.Migrations.M20220304_msg_quotes
|
||||
import Simplex.Chat.Migrations.M20220321_chat_item_edited
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (eitherToMaybe)
|
||||
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..), SMPServer (..))
|
||||
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri, AgentMsgId, ConnId, InvitationId, MsgMeta (..), SMPServer (..))
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, withTransaction)
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
@@ -190,7 +195,8 @@ schemaMigrations =
|
||||
("20220224_messages_fks", m20220224_messages_fks),
|
||||
("20220301_smp_servers", m20220301_smp_servers),
|
||||
("20220302_profile_images", m20220302_profile_images),
|
||||
("20220304_msg_quotes", m20220304_msg_quotes)
|
||||
("20220304_msg_quotes", m20220304_msg_quotes),
|
||||
("20220321_chat_item_edited", m20220321_chat_item_edited)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
@@ -1871,7 +1877,7 @@ getRcvFileTransfer_ db userId fileId =
|
||||
(userId, fileId)
|
||||
where
|
||||
rcvFileTransfer ::
|
||||
[(FileStatus, ConnReqInvitation, String, Integer, Integer, Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe Int64, Maybe AgentConnId)] ->
|
||||
[(FileStatus, AConnectionRequestUri, String, Integer, Integer, Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe Int64, Maybe AgentConnId)] ->
|
||||
Either StoreError RcvFileTransfer
|
||||
rcvFileTransfer [(fileStatus', fileConnReq, fileName, fileSize, chunkSize, contactName_, memberName_, filePath_, connId_, agentConnId_)] =
|
||||
let fileInv = FileInvitation {fileName, fileSize, fileConnReq}
|
||||
@@ -2182,7 +2188,7 @@ createNewSndChatItem st user chatDirection SndMessage {msgId, sharedMsgId} ciCon
|
||||
quoteRow :: NewQuoteRow
|
||||
quoteRow = case quotedItem of
|
||||
Nothing -> (Nothing, Nothing, Nothing, Nothing, Nothing)
|
||||
Just (CIQuote {chatDir, sharedMsgId = quotedSharedMsgId, sentAt, content}) ->
|
||||
Just CIQuote {chatDir, sharedMsgId = quotedSharedMsgId, sentAt, content} ->
|
||||
uncurry (quotedSharedMsgId,Just sentAt,Just content,,) $ case chatDir of
|
||||
CIQDirectSnd -> (Just True, Nothing)
|
||||
CIQDirectRcv -> (Just False, Nothing)
|
||||
@@ -2320,7 +2326,8 @@ chatItemTs (CChatItem _ ChatItem {meta = CIMeta {itemTs}}) = itemTs
|
||||
getDirectChatPreviews_ :: DB.Connection -> User -> IO [AChat]
|
||||
getDirectChatPreviews_ db User {userId} = do
|
||||
tz <- getCurrentTimeZone
|
||||
map (toDirectChatPreview tz)
|
||||
currentTs <- getCurrentTime
|
||||
map (toDirectChatPreview tz currentTs)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
@@ -2333,7 +2340,7 @@ getDirectChatPreviews_ db User {userId} = do
|
||||
-- ChatStats
|
||||
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0),
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.created_at,
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM contacts ct
|
||||
@@ -2370,17 +2377,18 @@ getDirectChatPreviews_ db User {userId} = do
|
||||
|]
|
||||
(CISRcvNew, userId, ConnReady, ConnSndReady)
|
||||
where
|
||||
toDirectChatPreview :: TimeZone -> ContactRow :. ConnectionRow :. ChatStatsRow :. MaybeChatItemRow :. QuoteRow -> AChat
|
||||
toDirectChatPreview tz (contactRow :. connRow :. statsRow :. ciRow_) =
|
||||
toDirectChatPreview :: TimeZone -> UTCTime -> ContactRow :. ConnectionRow :. ChatStatsRow :. MaybeChatItemRow :. QuoteRow -> AChat
|
||||
toDirectChatPreview tz currentTs (contactRow :. connRow :. statsRow :. ciRow_) =
|
||||
let contact = toContact $ contactRow :. connRow
|
||||
ci_ = toDirectChatItemList tz ciRow_
|
||||
ci_ = toDirectChatItemList tz currentTs ciRow_
|
||||
stats = toChatStats statsRow
|
||||
in AChat SCTDirect $ Chat (DirectChat contact) ci_ stats
|
||||
|
||||
getGroupChatPreviews_ :: DB.Connection -> User -> IO [AChat]
|
||||
getGroupChatPreviews_ db User {userId, userContactId} = do
|
||||
tz <- getCurrentTimeZone
|
||||
map (toGroupChatPreview tz)
|
||||
currentTs <- getCurrentTime
|
||||
map (toGroupChatPreview tz currentTs)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
@@ -2394,7 +2402,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
|
||||
-- ChatStats
|
||||
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0),
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.created_at,
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at,
|
||||
-- Maybe GroupMember - sender
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
|
||||
m.member_status, m.invited_by, m.local_display_name, m.contact_id,
|
||||
@@ -2433,10 +2441,10 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
|
||||
|]
|
||||
(CISRcvNew, userId, userContactId)
|
||||
where
|
||||
toGroupChatPreview :: TimeZone -> GroupInfoRow :. ChatStatsRow :. MaybeGroupChatItemRow -> AChat
|
||||
toGroupChatPreview tz (groupInfoRow :. statsRow :. ciRow_) =
|
||||
toGroupChatPreview :: TimeZone -> UTCTime -> GroupInfoRow :. ChatStatsRow :. MaybeGroupChatItemRow -> AChat
|
||||
toGroupChatPreview tz currentTs (groupInfoRow :. statsRow :. ciRow_) =
|
||||
let groupInfo = toGroupInfo userContactId groupInfoRow
|
||||
ci_ = toGroupChatItemList tz userContactId ciRow_
|
||||
ci_ = toGroupChatItemList tz currentTs userContactId ciRow_
|
||||
stats = toChatStats statsRow
|
||||
in AChat SCTGroup $ Chat (GroupChat groupInfo) ci_ stats
|
||||
|
||||
@@ -2480,13 +2488,14 @@ getDirectChatLast_ db User {userId} contactId count = do
|
||||
getDirectChatItemsLast_ :: IO (Either StoreError [CChatItem 'CTDirect])
|
||||
getDirectChatItemsLast_ = do
|
||||
tz <- getCurrentTimeZone
|
||||
mapM (toDirectChatItem tz)
|
||||
currentTs <- getCurrentTime
|
||||
mapM (toDirectChatItem tz currentTs)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.created_at,
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM chat_items i
|
||||
@@ -2507,13 +2516,14 @@ getDirectChatAfter_ db User {userId} contactId afterChatItemId count = do
|
||||
getDirectChatItemsAfter_ :: IO (Either StoreError [CChatItem 'CTDirect])
|
||||
getDirectChatItemsAfter_ = do
|
||||
tz <- getCurrentTimeZone
|
||||
mapM (toDirectChatItem tz)
|
||||
currentTs <- getCurrentTime
|
||||
mapM (toDirectChatItem tz currentTs)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.created_at,
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM chat_items i
|
||||
@@ -2534,13 +2544,14 @@ getDirectChatBefore_ db User {userId} contactId beforeChatItemId count = do
|
||||
getDirectChatItemsBefore_ :: IO (Either StoreError [CChatItem 'CTDirect])
|
||||
getDirectChatItemsBefore_ = do
|
||||
tz <- getCurrentTimeZone
|
||||
mapM (toDirectChatItem tz)
|
||||
currentTs <- getCurrentTime
|
||||
mapM (toDirectChatItem tz currentTs)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.created_at,
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM chat_items i
|
||||
@@ -2633,13 +2644,14 @@ getGroupChatLast_ db user@User {userId, userContactId} groupId count = do
|
||||
getGroupChatItemsLast_ :: IO (Either StoreError [CChatItem 'CTGroup])
|
||||
getGroupChatItemsLast_ = do
|
||||
tz <- getCurrentTimeZone
|
||||
mapM (toGroupChatItem tz userContactId)
|
||||
currentTs <- getCurrentTime
|
||||
mapM (toGroupChatItem tz currentTs userContactId)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.created_at,
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at,
|
||||
-- GroupMember
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
|
||||
m.member_status, m.invited_by, m.local_display_name, m.contact_id,
|
||||
@@ -2672,13 +2684,14 @@ getGroupChatAfter_ db user@User {userId, userContactId} groupId afterChatItemId
|
||||
getGroupChatItemsAfter_ :: IO (Either StoreError [CChatItem 'CTGroup])
|
||||
getGroupChatItemsAfter_ = do
|
||||
tz <- getCurrentTimeZone
|
||||
mapM (toGroupChatItem tz userContactId)
|
||||
currentTs <- getCurrentTime
|
||||
mapM (toGroupChatItem tz currentTs userContactId)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.created_at,
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at,
|
||||
-- GroupMember
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
|
||||
m.member_status, m.invited_by, m.local_display_name, m.contact_id,
|
||||
@@ -2711,13 +2724,14 @@ getGroupChatBefore_ db user@User {userId, userContactId} groupId beforeChatItemI
|
||||
getGroupChatItemsBefore_ :: IO (Either StoreError [CChatItem 'CTGroup])
|
||||
getGroupChatItemsBefore_ = do
|
||||
tz <- getCurrentTimeZone
|
||||
mapM (toGroupChatItem tz userContactId)
|
||||
currentTs <- getCurrentTime
|
||||
mapM (toGroupChatItem tz currentTs userContactId)
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.created_at,
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at,
|
||||
-- GroupMember
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
|
||||
m.member_status, m.invited_by, m.local_display_name, m.contact_id,
|
||||
@@ -2810,8 +2824,8 @@ getChatItemIdByAgentMsgId st connId msgId =
|
||||
|]
|
||||
(connId, msgId)
|
||||
|
||||
updateDirectChatItem :: forall m d. (StoreMonad m, MsgDirectionI d) => SQLiteStore -> UserId -> Int64 -> ChatItemId -> CIStatus d -> m (ChatItem 'CTDirect d)
|
||||
updateDirectChatItem st userId contactId itemId itemStatus =
|
||||
updateDirectChatItemStatus :: forall m d. (StoreMonad m, MsgDirectionI d) => SQLiteStore -> UserId -> Int64 -> ChatItemId -> CIStatus d -> m (ChatItem 'CTDirect d)
|
||||
updateDirectChatItemStatus st userId contactId itemId itemStatus =
|
||||
liftIOEither . withTransaction st $ \db -> runExceptT $ do
|
||||
ci <- ExceptT $ (correctDir =<<) <$> getDirectChatItem_ db userId contactId itemId
|
||||
currentTs <- liftIO getCurrentTime
|
||||
@@ -2821,6 +2835,50 @@ updateDirectChatItem st userId contactId itemId itemStatus =
|
||||
correctDir :: CChatItem c -> Either StoreError (ChatItem c d)
|
||||
correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci
|
||||
|
||||
updateDirectChatItem :: forall m d. (StoreMonad m, MsgDirectionI d) => SQLiteStore -> UserId -> Int64 -> ChatItemId -> CIContent d -> MessageId -> m (ChatItem 'CTDirect d)
|
||||
updateDirectChatItem st userId contactId itemId newContent msgId =
|
||||
liftIOEither . withTransaction st $ \db -> updateDirectChatItem_ db userId contactId itemId newContent msgId
|
||||
|
||||
updateDirectChatItem_ :: forall d. (MsgDirectionI d) => DB.Connection -> UserId -> Int64 -> ChatItemId -> CIContent d -> MessageId -> IO (Either StoreError (ChatItem 'CTDirect d))
|
||||
updateDirectChatItem_ db userId contactId itemId newContent msgId = runExceptT $ do
|
||||
ci <- ExceptT $ (correctDir =<<) <$> getDirectChatItem_ db userId contactId itemId
|
||||
currentTs <- liftIO getCurrentTime
|
||||
let newText = ciContentToText newContent
|
||||
liftIO $
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE chat_items
|
||||
SET item_content = ?, item_text = ?, item_edited = 1, updated_at = ?
|
||||
WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?
|
||||
|]
|
||||
(newContent, newText, currentTs, userId, contactId, itemId)
|
||||
liftIO $ DB.execute db "INSERT INTO chat_item_messages (chat_item_id, message_id, created_at, updated_at) VALUES (?,?,?,?)" (itemId, msgId, currentTs, currentTs)
|
||||
pure ci {content = newContent, meta = (meta ci) {itemText = newText, itemEdited = True}, formattedText = parseMaybeMarkdownList newText}
|
||||
where
|
||||
correctDir :: CChatItem c -> Either StoreError (ChatItem c d)
|
||||
correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci
|
||||
|
||||
updateDirectChatItemByMsgId :: forall m d. (StoreMonad m, MsgDirectionI d) => SQLiteStore -> UserId -> Int64 -> SharedMsgId -> CIContent d -> MessageId -> m (ChatItem 'CTDirect d)
|
||||
updateDirectChatItemByMsgId st userId contactId sharedMsgId newContent msgId =
|
||||
liftIOEither . withTransaction st $ \db -> runExceptT $ do
|
||||
itemId <- ExceptT $ getDirectChatItemIdBySharedMsgId_ db userId contactId sharedMsgId
|
||||
liftIOEither $ updateDirectChatItem_ db userId contactId itemId newContent msgId
|
||||
|
||||
getDirectChatItemIdBySharedMsgId_ :: DB.Connection -> UserId -> Int64 -> SharedMsgId -> IO (Either StoreError Int64)
|
||||
getDirectChatItemIdBySharedMsgId_ db userId contactId sharedMsgId =
|
||||
firstRow fromOnly (SEChatItemSharedMsgIdNotFound sharedMsgId) $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT chat_item_id
|
||||
FROM chat_items
|
||||
WHERE user_id = ? AND contact_id = ? AND shared_msg_id = ?
|
||||
ORDER BY chat_item_id DESC
|
||||
LIMIT 1
|
||||
|]
|
||||
(userId, contactId, sharedMsgId)
|
||||
|
||||
getDirectChatItem :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> ChatItemId -> m (CChatItem 'CTDirect)
|
||||
getDirectChatItem st userId contactId itemId =
|
||||
liftIOEither . withTransaction st $ \db -> getDirectChatItem_ db userId contactId itemId
|
||||
@@ -2828,7 +2886,8 @@ getDirectChatItem st userId contactId itemId =
|
||||
getDirectChatItem_ :: DB.Connection -> UserId -> Int64 -> ChatItemId -> IO (Either StoreError (CChatItem 'CTDirect))
|
||||
getDirectChatItem_ db userId contactId itemId = do
|
||||
tz <- getCurrentTimeZone
|
||||
join <$> firstRow (toDirectChatItem tz) (SEChatItemNotFound itemId) getItem
|
||||
currentTs <- getCurrentTime
|
||||
join <$> firstRow (toDirectChatItem tz currentTs) (SEChatItemNotFound itemId) getItem
|
||||
where
|
||||
getItem =
|
||||
DB.query
|
||||
@@ -2836,7 +2895,7 @@ getDirectChatItem_ db userId contactId itemId = do
|
||||
[sql|
|
||||
SELECT
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.created_at,
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at,
|
||||
-- DirectQuote
|
||||
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent
|
||||
FROM chat_items i
|
||||
@@ -2860,19 +2919,67 @@ getDirectChatItemIdByText st userId contactId msgDir quotedMsg =
|
||||
|]
|
||||
(userId, contactId, msgDir, quotedMsg <> "%")
|
||||
|
||||
getGroupChatItem :: StoreMonad m => SQLiteStore -> User -> Int64 -> ChatItemId -> m (CChatItem 'CTGroup)
|
||||
getGroupChatItem st User {userId, userContactId} groupId itemId =
|
||||
liftIOEither . withTransaction st $ \db -> do
|
||||
tz <- getCurrentTimeZone
|
||||
join <$> firstRow (toGroupChatItem tz userContactId) (SEChatItemNotFound itemId) (getItem db)
|
||||
updateGroupChatItem :: forall m d. (StoreMonad m, MsgDirectionI d) => SQLiteStore -> User -> Int64 -> ChatItemId -> CIContent d -> MessageId -> m (ChatItem 'CTGroup d)
|
||||
updateGroupChatItem st user groupId itemId newContent msgId =
|
||||
liftIOEither . withTransaction st $ \db -> updateGroupChatItem_ db user groupId itemId newContent msgId
|
||||
|
||||
updateGroupChatItem_ :: forall d. (MsgDirectionI d) => DB.Connection -> User -> Int64 -> ChatItemId -> CIContent d -> MessageId -> IO (Either StoreError (ChatItem 'CTGroup d))
|
||||
updateGroupChatItem_ db user@User {userId} groupId itemId newContent msgId = runExceptT $ do
|
||||
ci <- ExceptT $ (correctDir =<<) <$> getGroupChatItem_ db user groupId itemId
|
||||
currentTs <- liftIO getCurrentTime
|
||||
let newText = ciContentToText newContent
|
||||
liftIO $
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE chat_items
|
||||
SET item_content = ?, item_text = ?, item_edited = 1, updated_at = ?
|
||||
WHERE user_id = ? AND group_id = ? AND chat_item_id = ?
|
||||
|]
|
||||
(newContent, newText, currentTs, userId, groupId, itemId)
|
||||
liftIO $ DB.execute db "INSERT INTO chat_item_messages (chat_item_id, message_id, created_at, updated_at) VALUES (?,?,?,?)" (itemId, msgId, currentTs, currentTs)
|
||||
pure ci {content = newContent, meta = (meta ci) {itemText = newText, itemEdited = True}, formattedText = parseMaybeMarkdownList newText}
|
||||
where
|
||||
getItem db =
|
||||
correctDir :: CChatItem c -> Either StoreError (ChatItem c d)
|
||||
correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci
|
||||
|
||||
updateGroupChatItemByMsgId :: forall m d. (StoreMonad m, MsgDirectionI d) => SQLiteStore -> User -> Int64 -> SharedMsgId -> CIContent d -> MessageId -> m (ChatItem 'CTGroup d)
|
||||
updateGroupChatItemByMsgId st user groupId sharedMsgId newContent msgId =
|
||||
liftIOEither . withTransaction st $ \db -> runExceptT $ do
|
||||
itemId <- ExceptT $ getGroupChatItemIdBySharedMsgId_ db user groupId sharedMsgId
|
||||
liftIOEither $ updateGroupChatItem_ db user groupId itemId newContent msgId
|
||||
|
||||
getGroupChatItemIdBySharedMsgId_ :: DB.Connection -> User -> Int64 -> SharedMsgId -> IO (Either StoreError Int64)
|
||||
getGroupChatItemIdBySharedMsgId_ db User {userId} groupId sharedMsgId =
|
||||
firstRow fromOnly (SEChatItemSharedMsgIdNotFound sharedMsgId) $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT chat_item_id
|
||||
FROM chat_items
|
||||
WHERE user_id = ? AND group_id = ? AND shared_msg_id = ?
|
||||
ORDER BY chat_item_id DESC
|
||||
LIMIT 1
|
||||
|]
|
||||
(userId, groupId, sharedMsgId)
|
||||
|
||||
getGroupChatItem :: StoreMonad m => SQLiteStore -> User -> Int64 -> ChatItemId -> m (CChatItem 'CTGroup)
|
||||
getGroupChatItem st user groupId itemId =
|
||||
liftIOEither . withTransaction st $ \db -> getGroupChatItem_ db user groupId itemId
|
||||
|
||||
getGroupChatItem_ :: DB.Connection -> User -> Int64 -> ChatItemId -> IO (Either StoreError (CChatItem 'CTGroup))
|
||||
getGroupChatItem_ db User {userId, userContactId} groupId itemId = do
|
||||
tz <- getCurrentTimeZone
|
||||
currentTs <- liftIO getCurrentTime
|
||||
join <$> firstRow (toGroupChatItem tz currentTs userContactId) (SEChatItemNotFound itemId) getItem
|
||||
where
|
||||
getItem =
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT
|
||||
-- ChatItem
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.created_at,
|
||||
i.chat_item_id, i.item_ts, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_edited, i.created_at,
|
||||
-- GroupMember
|
||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category,
|
||||
m.member_status, m.invited_by, m.local_display_name, m.contact_id,
|
||||
@@ -2967,9 +3074,9 @@ type ChatStatsRow = (Int, ChatItemId)
|
||||
toChatStats :: ChatStatsRow -> ChatStats
|
||||
toChatStats (unreadCount, minUnreadItemId) = ChatStats {unreadCount, minUnreadItemId}
|
||||
|
||||
type ChatItemRow = (Int64, ChatItemTs, ACIContent, Text, ACIStatus, Maybe SharedMsgId, UTCTime)
|
||||
type ChatItemRow = (Int64, ChatItemTs, ACIContent, Text, ACIStatus, Maybe SharedMsgId, Bool, Maybe Bool, UTCTime)
|
||||
|
||||
type MaybeChatItemRow = (Maybe Int64, Maybe ChatItemTs, Maybe ACIContent, Maybe Text, Maybe ACIStatus, Maybe SharedMsgId, Maybe UTCTime)
|
||||
type MaybeChatItemRow = (Maybe Int64, Maybe ChatItemTs, Maybe ACIContent, Maybe Text, Maybe ACIStatus, Maybe SharedMsgId, Maybe Bool, Maybe Bool, Maybe UTCTime)
|
||||
|
||||
type QuoteRow = (Maybe ChatItemId, Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool)
|
||||
|
||||
@@ -2990,8 +3097,8 @@ toQuote :: QuoteRow -> Maybe (CIQDirection c) -> Maybe (CIQuote c)
|
||||
toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir =
|
||||
CIQuote <$> dir <*> pure quotedItemId <*> pure quotedSharedMsgId <*> quotedSentAt <*> quotedMsgContent <*> (parseMaybeMarkdownList . msgContentText <$> quotedMsgContent)
|
||||
|
||||
toDirectChatItem :: TimeZone -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
|
||||
toDirectChatItem tz ((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, createdAt) :. quoteRow) =
|
||||
toDirectChatItem :: TimeZone -> UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
|
||||
toDirectChatItem tz currentTs ((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, itemDeleted, itemEdited, createdAt) :. quoteRow) =
|
||||
case (itemContent, itemStatus) of
|
||||
(ACIContent SMDSnd ciContent, ACIStatus SMDSnd ciStatus) -> Right $ cItem SMDSnd CIDirectSnd ciStatus ciContent
|
||||
(ACIContent SMDRcv ciContent, ACIStatus SMDRcv ciStatus) -> Right $ cItem SMDRcv CIDirectRcv ciStatus ciContent
|
||||
@@ -3002,12 +3109,12 @@ toDirectChatItem tz ((itemId, itemTs, itemContent, itemText, itemStatus, sharedM
|
||||
CChatItem d ChatItem {chatDir, meta = ciMeta ciStatus, content, formattedText = parseMaybeMarkdownList itemText, quotedItem = toDirectQuote quoteRow}
|
||||
badItem = Left $ SEBadChatItem itemId
|
||||
ciMeta :: CIStatus d -> CIMeta d
|
||||
ciMeta status = mkCIMeta itemId itemText status sharedMsgId tz itemTs createdAt
|
||||
ciMeta status = mkCIMeta itemId itemText status sharedMsgId itemDeleted (fromMaybe False itemEdited) tz currentTs itemTs createdAt
|
||||
|
||||
toDirectChatItemList :: TimeZone -> MaybeChatItemRow :. QuoteRow -> [CChatItem 'CTDirect]
|
||||
toDirectChatItemList tz ((Just itemId, Just itemTs, Just itemContent, Just itemText, Just itemStatus, sharedMsgId, Just createdAt) :. quoteRow) =
|
||||
either (const []) (: []) $ toDirectChatItem tz ((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, createdAt) :. quoteRow)
|
||||
toDirectChatItemList _ _ = []
|
||||
toDirectChatItemList :: TimeZone -> UTCTime -> MaybeChatItemRow :. QuoteRow -> [CChatItem 'CTDirect]
|
||||
toDirectChatItemList tz currentTs ((Just itemId, Just itemTs, Just itemContent, Just itemText, Just itemStatus, sharedMsgId, Just itemDeleted, itemEdited, Just createdAt) :. quoteRow) =
|
||||
either (const []) (: []) $ toDirectChatItem tz currentTs ((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, itemDeleted, itemEdited, createdAt) :. quoteRow)
|
||||
toDirectChatItemList _ _ _ = []
|
||||
|
||||
type GroupQuoteRow = QuoteRow :. MaybeGroupMemberRow
|
||||
|
||||
@@ -3021,8 +3128,8 @@ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction
|
||||
direction (Just False) Nothing = Just $ CIQGroupRcv Nothing
|
||||
direction _ _ = Nothing
|
||||
|
||||
toGroupChatItem :: TimeZone -> Int64 -> ChatItemRow :. MaybeGroupMemberRow :. GroupQuoteRow -> Either StoreError (CChatItem 'CTGroup)
|
||||
toGroupChatItem tz userContactId ((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, createdAt) :. memberRow_ :. quoteRow :. quotedMemberRow_) = do
|
||||
toGroupChatItem :: TimeZone -> UTCTime -> Int64 -> ChatItemRow :. MaybeGroupMemberRow :. GroupQuoteRow -> Either StoreError (CChatItem 'CTGroup)
|
||||
toGroupChatItem tz currentTs userContactId ((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, itemDeleted, itemEdited, createdAt) :. memberRow_ :. quoteRow :. quotedMemberRow_) = do
|
||||
let member_ = toMaybeGroupMember userContactId memberRow_
|
||||
let quotedMember_ = toMaybeGroupMember userContactId quotedMemberRow_
|
||||
case (itemContent, itemStatus, member_) of
|
||||
@@ -3035,12 +3142,12 @@ toGroupChatItem tz userContactId ((itemId, itemTs, itemContent, itemText, itemSt
|
||||
CChatItem d ChatItem {chatDir, meta = ciMeta ciStatus, content, formattedText = parseMaybeMarkdownList itemText, quotedItem = toGroupQuote quoteRow quotedMember_}
|
||||
badItem = Left $ SEBadChatItem itemId
|
||||
ciMeta :: CIStatus d -> CIMeta d
|
||||
ciMeta status = mkCIMeta itemId itemText status sharedMsgId tz itemTs createdAt
|
||||
ciMeta status = mkCIMeta itemId itemText status sharedMsgId itemDeleted (fromMaybe False itemEdited) tz currentTs itemTs createdAt
|
||||
|
||||
toGroupChatItemList :: TimeZone -> Int64 -> MaybeGroupChatItemRow -> [CChatItem 'CTGroup]
|
||||
toGroupChatItemList tz userContactId ((Just itemId, Just itemTs, Just itemContent, Just itemText, Just itemStatus, sharedMsgId, Just createdAt) :. memberRow_ :. quoteRow :. quotedMemberRow_) =
|
||||
either (const []) (: []) $ toGroupChatItem tz userContactId ((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, createdAt) :. memberRow_ :. quoteRow :. quotedMemberRow_)
|
||||
toGroupChatItemList _ _ _ = []
|
||||
toGroupChatItemList :: TimeZone -> UTCTime -> Int64 -> MaybeGroupChatItemRow -> [CChatItem 'CTGroup]
|
||||
toGroupChatItemList tz currentTs userContactId ((Just itemId, Just itemTs, Just itemContent, Just itemText, Just itemStatus, sharedMsgId, Just itemDeleted, itemEdited, Just createdAt) :. memberRow_ :. quoteRow :. quotedMemberRow_) =
|
||||
either (const []) (: []) $ toGroupChatItem tz currentTs userContactId ((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, itemDeleted, itemEdited, createdAt) :. memberRow_ :. quoteRow :. quotedMemberRow_)
|
||||
toGroupChatItemList _ _ _ _ = []
|
||||
|
||||
getSMPServers :: MonadUnliftIO m => SQLiteStore -> User -> m [SMPServer]
|
||||
getSMPServers st User {userId} =
|
||||
@@ -3160,6 +3267,7 @@ data StoreError
|
||||
| SEBadChatItem {itemId :: ChatItemId}
|
||||
| SEChatItemNotFound {itemId :: ChatItemId}
|
||||
| SEQuotedChatItemNotFound
|
||||
| SEChatItemSharedMsgIdNotFound {sharedMsgId :: SharedMsgId}
|
||||
deriving (Show, Exception, Generic)
|
||||
|
||||
instance ToJSON StoreError where
|
||||
|
||||
@@ -31,7 +31,7 @@ import Database.SQLite.Simple.Internal (Field (..))
|
||||
import Database.SQLite.Simple.Ok (Ok (Ok))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Messaging.Agent.Protocol (ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId)
|
||||
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri, ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (fromTextField_)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
@@ -522,7 +522,7 @@ type FileTransferId = Int64
|
||||
data FileInvitation = FileInvitation
|
||||
{ fileName :: String,
|
||||
fileSize :: Integer,
|
||||
fileConnReq :: ConnReqInvitation
|
||||
fileConnReq :: AConnectionRequestUri
|
||||
}
|
||||
deriving (Eq, Show, Generic, FromJSON)
|
||||
|
||||
|
||||
@@ -48,7 +48,9 @@ responseToView testView = \case
|
||||
CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat]
|
||||
CRUserSMPServers smpServers -> viewSMPServers smpServers testView
|
||||
CRNewChatItem (AChatItem _ _ chat item) -> viewChatItem chat item
|
||||
CRChatItemUpdated _ -> []
|
||||
CRChatItemStatusUpdated _ -> []
|
||||
CRChatItemUpdated (AChatItem _ _ chat item) -> viewMessageUpdate chat item
|
||||
CRChatItemDeleted _ -> [] -- TODO
|
||||
CRMsgIntegrityError mErr -> viewMsgIntegrityError mErr
|
||||
CRCmdAccepted _ -> []
|
||||
CRCmdOk -> ["ok"]
|
||||
@@ -166,11 +168,13 @@ viewChatItem chat ChatItem {chatDir, meta, content, quotedItem} = case chat of
|
||||
DirectChat c -> case chatDir of
|
||||
CIDirectSnd -> case content of
|
||||
CISndMsgContent mc -> viewSentMessage to quote mc meta
|
||||
CISndMsgDeleted _mc -> []
|
||||
CISndFileInvitation fId fPath -> viewSentFileInvitation to fId fPath meta
|
||||
where
|
||||
to = ttyToContact' c
|
||||
CIDirectRcv -> case content of
|
||||
CIRcvMsgContent mc -> viewReceivedMessage from quote meta mc
|
||||
CIRcvMsgDeleted _mc -> []
|
||||
CIRcvFileInvitation ft -> viewReceivedFileInvitation from meta ft
|
||||
where
|
||||
from = ttyFromContact' c
|
||||
@@ -179,33 +183,62 @@ viewChatItem chat ChatItem {chatDir, meta, content, quotedItem} = case chat of
|
||||
GroupChat g -> case chatDir of
|
||||
CIGroupSnd -> case content of
|
||||
CISndMsgContent mc -> viewSentMessage to quote mc meta
|
||||
CISndMsgDeleted _mc -> []
|
||||
CISndFileInvitation fId fPath -> viewSentFileInvitation to fId fPath meta
|
||||
where
|
||||
to = ttyToGroup g
|
||||
CIGroupRcv m -> case content of
|
||||
CIRcvMsgContent mc -> viewReceivedMessage from quote meta mc
|
||||
CIRcvMsgDeleted _mc -> []
|
||||
CIRcvFileInvitation ft -> viewReceivedFileInvitation from meta ft
|
||||
where
|
||||
from = ttyFromGroup' g m
|
||||
where
|
||||
quote = maybe [] (groupQuote g) quotedItem
|
||||
_ -> []
|
||||
where
|
||||
directQuote :: forall d'. MsgDirectionI d' => CIDirection 'CTDirect d' -> CIQuote 'CTDirect -> [StyledString]
|
||||
directQuote _ CIQuote {content = qmc, chatDir = qouteDir} =
|
||||
quoteText qmc $ if toMsgDirection (msgDirection @d') == quoteMsgDirection qouteDir then ">>" else ">"
|
||||
groupQuote :: GroupInfo -> CIQuote 'CTGroup -> [StyledString]
|
||||
groupQuote g CIQuote {content = qmc, chatDir = quoteDir} = quoteText qmc . ttyQuotedMember $ sentByMember g quoteDir
|
||||
sentByMember :: GroupInfo -> CIQDirection 'CTGroup -> Maybe GroupMember
|
||||
sentByMember GroupInfo {membership} = \case
|
||||
CIQGroupSnd -> Just membership
|
||||
CIQGroupRcv m -> m
|
||||
quoteText qmc sentBy = prependFirst (sentBy <> " ") $ msgPreview qmc
|
||||
msgPreview = msgPlain . preview . msgContentText
|
||||
|
||||
viewMessageUpdate :: MsgDirectionI d => ChatInfo c -> ChatItem c d -> [StyledString]
|
||||
viewMessageUpdate chat ChatItem {chatDir, meta, content, quotedItem} = case chat of
|
||||
DirectChat Contact {localDisplayName = c} -> case chatDir of
|
||||
CIDirectRcv -> case content of
|
||||
CIRcvMsgContent mc -> viewReceivedMessage from quote meta mc
|
||||
_ -> []
|
||||
where
|
||||
preview t
|
||||
| T.length t <= 60 = t
|
||||
| otherwise = t <> "..."
|
||||
from = ttyFromContactEdited c
|
||||
quote = maybe [] (directQuote chatDir) quotedItem
|
||||
CIDirectSnd -> []
|
||||
GroupChat g -> case chatDir of
|
||||
CIGroupRcv GroupMember {localDisplayName = m} -> case content of
|
||||
CIRcvMsgContent mc -> viewReceivedMessage from quote meta mc
|
||||
_ -> []
|
||||
where
|
||||
from = ttyFromGroupEdited g m
|
||||
quote = maybe [] (groupQuote g) quotedItem
|
||||
CIGroupSnd -> []
|
||||
where
|
||||
_ -> []
|
||||
|
||||
directQuote :: forall d'. MsgDirectionI d' => CIDirection 'CTDirect d' -> CIQuote 'CTDirect -> [StyledString]
|
||||
directQuote _ CIQuote {content = qmc, chatDir = quoteDir} =
|
||||
quoteText qmc $ if toMsgDirection (msgDirection @d') == quoteMsgDirection quoteDir then ">>" else ">"
|
||||
|
||||
groupQuote :: GroupInfo -> CIQuote 'CTGroup -> [StyledString]
|
||||
groupQuote g CIQuote {content = qmc, chatDir = quoteDir} = quoteText qmc . ttyQuotedMember $ sentByMember g quoteDir
|
||||
|
||||
sentByMember :: GroupInfo -> CIQDirection 'CTGroup -> Maybe GroupMember
|
||||
sentByMember GroupInfo {membership} = \case
|
||||
CIQGroupSnd -> Just membership
|
||||
CIQGroupRcv m -> m
|
||||
|
||||
quoteText :: MsgContent -> StyledString -> [StyledString]
|
||||
quoteText qmc sentBy = prependFirst (sentBy <> " ") $ msgPreview qmc
|
||||
|
||||
msgPreview :: MsgContent -> [StyledString]
|
||||
msgPreview = msgPlain . preview . msgContentText
|
||||
where
|
||||
preview t
|
||||
| T.length t <= 60 = t
|
||||
| otherwise = t <> "..."
|
||||
|
||||
viewMsgIntegrityError :: MsgErrorType -> [StyledString]
|
||||
viewMsgIntegrityError err = msgError $ case err of
|
||||
@@ -552,6 +585,7 @@ viewChatError = \case
|
||||
CEFileRcvChunk e -> ["error receiving file: " <> plain e]
|
||||
CEFileInternal e -> ["file error: " <> plain e]
|
||||
CEInvalidQuote -> ["cannot reply to this message"]
|
||||
CEInvalidMessageUpdate -> ["cannot update this message"]
|
||||
CEAgentVersion -> ["unsupported agent version"]
|
||||
CECommandError e -> ["bad chat command: " <> plain e]
|
||||
-- e -> ["chat error: " <> sShow e]
|
||||
@@ -602,6 +636,9 @@ ttyToContact c = styled (colored Cyan) $ "@" <> c <> " "
|
||||
ttyFromContact :: ContactName -> StyledString
|
||||
ttyFromContact c = ttyFrom $ c <> "> "
|
||||
|
||||
ttyFromContactEdited :: ContactName -> StyledString
|
||||
ttyFromContactEdited c = ttyFrom $ c <> "> [edited] "
|
||||
|
||||
ttyToContact' :: Contact -> StyledString
|
||||
ttyToContact' Contact {localDisplayName = c} = ttyToContact c
|
||||
|
||||
@@ -633,6 +670,9 @@ ttyFullGroup GroupInfo {localDisplayName = g, groupProfile = GroupProfile {fullN
|
||||
ttyFromGroup :: GroupInfo -> ContactName -> StyledString
|
||||
ttyFromGroup GroupInfo {localDisplayName = g} c = ttyFrom $ "#" <> g <> " " <> c <> "> "
|
||||
|
||||
ttyFromGroupEdited :: GroupInfo -> ContactName -> StyledString
|
||||
ttyFromGroupEdited GroupInfo {localDisplayName = g} c = ttyFrom $ "#" <> g <> " " <> c <> "> [edited] "
|
||||
|
||||
ttyFrom :: Text -> StyledString
|
||||
ttyFrom = styled $ colored Yellow
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ packages:
|
||||
#
|
||||
extra-deps:
|
||||
- cryptostore-0.2.1.0@sha256:9896e2984f36a1c8790f057fd5ce3da4cbcaf8aa73eb2d9277916886978c5b19,3881
|
||||
- network-3.1.2.7@sha256:e3d78b13db9512aeb106e44a334ab42b7aa48d26c097299084084cb8be5c5568,4888
|
||||
- simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079
|
||||
- tls-1.5.7@sha256:1cc30253a9696b65a9cafc0317fbf09f7dcea15e3a145ed6c9c0e28c632fa23a,6991
|
||||
# below hackage dependancies are to update Aeson to 2.0.3
|
||||
@@ -48,7 +49,7 @@ extra-deps:
|
||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||
# - ../simplexmq
|
||||
- github: simplex-chat/simplexmq
|
||||
commit: 5c6ec96d6477371d8e617bcc71e6ecbcdd5c78cc
|
||||
commit: 800581b2bf5dacb2134dfda751be08cbf78df978
|
||||
# - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977
|
||||
- github: simplex-chat/aeson
|
||||
commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7
|
||||
|
||||
@@ -35,6 +35,7 @@ chatTests = do
|
||||
describe "direct messages" $ do
|
||||
it "add contact and send/receive message" testAddContact
|
||||
it "direct message quoted replies" testDirectMessageQuotedReply
|
||||
it "direct message update" testDirectMessageUpdate
|
||||
describe "chat groups" $ do
|
||||
it "add contacts, create group and send/receive messages" testGroup
|
||||
it "create and join group with 4 members" testGroup2
|
||||
@@ -44,6 +45,7 @@ chatTests = do
|
||||
it "remove contact from group and add again" testGroupRemoveAdd
|
||||
it "list groups containing group invitations" testGroupList
|
||||
it "group message quoted replies" testGroupMessageQuotedReply
|
||||
it "group message update" testGroupMessageUpdate
|
||||
describe "user profiles" $ do
|
||||
it "update user profiles and notify contacts" testUpdateProfile
|
||||
it "update user profile with image" testUpdateProfileImage
|
||||
@@ -150,6 +152,59 @@ testDirectMessageQuotedReply = do
|
||||
bob #$> ("/_get chat @2 count=1", chat', [((1, "will tell more"), Just (1, "all good - you?"))])
|
||||
alice #$> ("/_get chat @2 count=1", chat', [((0, "will tell more"), Just (0, "all good - you?"))])
|
||||
|
||||
testDirectMessageUpdate :: IO ()
|
||||
testDirectMessageUpdate = do
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
connectUsers alice bob
|
||||
|
||||
-- msg id 1
|
||||
alice #> "@bob hello 🙂"
|
||||
bob <# "alice> hello 🙂"
|
||||
|
||||
-- msg id 2
|
||||
bob `send` "> @alice (hello) hi alice"
|
||||
bob <# "@alice > hello 🙂"
|
||||
bob <## " hi alice"
|
||||
alice <# "bob> > hello 🙂"
|
||||
alice <## " hi alice"
|
||||
|
||||
alice #$> ("/_get chat @2 count=100", chat', [((1, "hello 🙂"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂"))])
|
||||
bob #$> ("/_get chat @2 count=100", chat', [((0, "hello 🙂"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂"))])
|
||||
|
||||
alice ##> "/_update item @2 1 text hey 👋"
|
||||
bob <# "alice> [edited] hey 👋"
|
||||
|
||||
alice #$> ("/_get chat @2 count=100", chat', [((1, "hey 👋"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂"))])
|
||||
bob #$> ("/_get chat @2 count=100", chat', [((0, "hey 👋"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂"))])
|
||||
|
||||
-- msg id 3
|
||||
bob `send` "> @alice (hey) hey alice"
|
||||
bob <# "@alice > hey 👋"
|
||||
bob <## " hey alice"
|
||||
alice <# "bob> > hey 👋"
|
||||
alice <## " hey alice"
|
||||
|
||||
alice #$> ("/_get chat @2 count=100", chat', [((1, "hey 👋"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂")), ((0, "hey alice"), Just (1, "hey 👋"))])
|
||||
bob #$> ("/_get chat @2 count=100", chat', [((0, "hey 👋"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂")), ((1, "hey alice"), Just (0, "hey 👋"))])
|
||||
|
||||
alice ##> "/_update item @2 1 text greetings 🤝"
|
||||
bob <# "alice> [edited] greetings 🤝"
|
||||
|
||||
alice #$> ("/_get chat @2 count=100", chat', [((1, "greetings 🤝"), Nothing), ((0, "hi alice"), Just (1, "hello 🙂")), ((0, "hey alice"), Just (1, "hey 👋"))])
|
||||
bob #$> ("/_get chat @2 count=100", chat', [((0, "greetings 🤝"), Nothing), ((1, "hi alice"), Just (0, "hello 🙂")), ((1, "hey alice"), Just (0, "hey 👋"))])
|
||||
|
||||
bob ##> "/_update item @2 2 text hey Alice"
|
||||
alice <# "bob> [edited] > hello 🙂"
|
||||
alice <## " hey Alice"
|
||||
|
||||
bob ##> "/_update item @2 3 text greetings Alice"
|
||||
alice <# "bob> [edited] > hey 👋"
|
||||
alice <## " greetings Alice"
|
||||
|
||||
alice #$> ("/_get chat @2 count=100", chat', [((1, "greetings 🤝"), Nothing), ((0, "hey Alice"), Just (1, "hello 🙂")), ((0, "greetings Alice"), Just (1, "hey 👋"))])
|
||||
bob #$> ("/_get chat @2 count=100", chat', [((0, "greetings 🤝"), Nothing), ((1, "hey Alice"), Just (0, "hello 🙂")), ((1, "greetings Alice"), Just (0, "hey 👋"))])
|
||||
|
||||
testGroup :: IO ()
|
||||
testGroup =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
@@ -619,7 +674,7 @@ testGroupMessageQuotedReply =
|
||||
cath #$> ("/_get chat #1 count=1", chat', [((1, "hi there!"), Just (0, "hello, all good, you?"))])
|
||||
alice #$> ("/_get chat #1 count=1", chat', [((0, "hi there!"), Just (0, "hello, all good, you?"))])
|
||||
bob #$> ("/_get chat #1 count=1", chat', [((0, "hi there!"), Just (1, "hello, all good, you?"))])
|
||||
alice `send ` "> #team (will tell) go on"
|
||||
alice `send` "> #team (will tell) go on"
|
||||
alice <# "#team > bob will tell more"
|
||||
alice <## " go on"
|
||||
concurrently_
|
||||
@@ -632,6 +687,66 @@ testGroupMessageQuotedReply =
|
||||
cath <## " go on"
|
||||
)
|
||||
|
||||
testGroupMessageUpdate :: IO ()
|
||||
testGroupMessageUpdate = do
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
createGroup3 "team" alice bob cath
|
||||
alice #> "#team hello!"
|
||||
concurrently_
|
||||
(bob <# "#team alice> hello!")
|
||||
(cath <# "#team alice> hello!")
|
||||
|
||||
alice ##> "/_update item #1 1 text hey 👋"
|
||||
concurrently_
|
||||
(bob <# "#team alice> [edited] hey 👋")
|
||||
(cath <# "#team alice> [edited] hey 👋")
|
||||
|
||||
alice #$> ("/_get chat #1 count=100", chat', [((1, "hey 👋"), Nothing)])
|
||||
bob #$> ("/_get chat #1 count=100", chat', [((0, "hey 👋"), Nothing)])
|
||||
cath #$> ("/_get chat #1 count=100", chat', [((0, "hey 👋"), Nothing)])
|
||||
|
||||
threadDelay 1000000
|
||||
bob `send` "> #team @alice (hey) hi alice"
|
||||
bob <# "#team > alice hey 👋"
|
||||
bob <## " hi alice"
|
||||
concurrently_
|
||||
( do
|
||||
alice <# "#team bob> > alice hey 👋"
|
||||
alice <## " hi alice"
|
||||
)
|
||||
( do
|
||||
cath <# "#team bob> > alice hey 👋"
|
||||
cath <## " hi alice"
|
||||
)
|
||||
|
||||
alice #$> ("/_get chat #1 count=100", chat', [((1, "hey 👋"), Nothing), ((0, "hi alice"), Just (1, "hey 👋"))])
|
||||
bob #$> ("/_get chat #1 count=100", chat', [((0, "hey 👋"), Nothing), ((1, "hi alice"), Just (0, "hey 👋"))])
|
||||
cath #$> ("/_get chat #1 count=100", chat', [((0, "hey 👋"), Nothing), ((0, "hi alice"), Just (0, "hey 👋"))])
|
||||
|
||||
alice ##> "/_update item #1 1 text greetings 🤝"
|
||||
concurrently_
|
||||
(bob <# "#team alice> [edited] greetings 🤝")
|
||||
(cath <# "#team alice> [edited] greetings 🤝")
|
||||
|
||||
threadDelay 1000000
|
||||
cath `send` "> #team @alice (greetings) greetings!"
|
||||
cath <# "#team > alice greetings 🤝"
|
||||
cath <## " greetings!"
|
||||
concurrently_
|
||||
( do
|
||||
alice <# "#team cath> > alice greetings 🤝"
|
||||
alice <## " greetings!"
|
||||
)
|
||||
( do
|
||||
bob <# "#team cath> > alice greetings 🤝"
|
||||
bob <## " greetings!"
|
||||
)
|
||||
|
||||
alice #$> ("/_get chat #1 count=100", chat', [((1, "greetings 🤝"), Nothing), ((0, "hi alice"), Just (1, "hey 👋")), ((0, "greetings!"), Just (1, "greetings 🤝"))])
|
||||
bob #$> ("/_get chat #1 count=100", chat', [((0, "greetings 🤝"), Nothing), ((1, "hi alice"), Just (0, "hey 👋")), ((0, "greetings!"), Just (0, "greetings 🤝"))])
|
||||
cath #$> ("/_get chat #1 count=100", chat', [((0, "greetings 🤝"), Nothing), ((0, "hi alice"), Just (0, "hey 👋")), ((1, "greetings!"), Just (0, "greetings 🤝"))])
|
||||
|
||||
testUpdateProfile :: IO ()
|
||||
testUpdateProfile =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
@@ -679,11 +794,14 @@ testUpdateProfileImage =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
connectUsers alice bob
|
||||
-- Note we currently don't support removing profile image.
|
||||
alice ##> "/profile_image data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="
|
||||
alice <## "profile image updated"
|
||||
alice ##> "/profile_image"
|
||||
alice <## "profile image removed"
|
||||
alice ##> "/_profile {\"displayName\": \"alice2\", \"fullName\": \"\"}"
|
||||
alice <## "user profile is changed to alice2 (your contacts are notified)"
|
||||
bob <## "contact alice changed to alice2"
|
||||
bob <## "use @alice2 <message> to send messages"
|
||||
(bob </)
|
||||
|
||||
testFileTransfer :: IO ()
|
||||
|
||||
@@ -71,10 +71,10 @@ s ##==## msg = do
|
||||
s ==## msg
|
||||
|
||||
(==#) :: ByteString -> ChatMsgEvent -> Expectation
|
||||
s ==# msg = s ==## (ChatMessage Nothing msg)
|
||||
s ==# msg = s ==## ChatMessage Nothing msg
|
||||
|
||||
(#==) :: ByteString -> ChatMsgEvent -> Expectation
|
||||
s #== msg = s ##== (ChatMessage Nothing msg)
|
||||
s #== msg = s ##== ChatMessage Nothing msg
|
||||
|
||||
(#==#) :: ByteString -> ChatMsgEvent -> Expectation
|
||||
s #==# msg = do
|
||||
@@ -93,23 +93,22 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
it "x.msg.new" $ "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## (ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew . MCSimple $ MCText "hello"))
|
||||
it "x.msg.new" $
|
||||
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}"
|
||||
##==## ( ChatMessage
|
||||
(Just $ SharedMsgId "\1\2\3\4")
|
||||
( XMsgNew $
|
||||
MCQuote
|
||||
( QuotedMsg
|
||||
(MsgRef (Just $ SharedMsgId "\5\6\7\8") (systemToUTCTime $ MkSystemTime 1 1) True Nothing)
|
||||
$ MCText "hello there!"
|
||||
)
|
||||
(MCText "hello to you too")
|
||||
)
|
||||
)
|
||||
##==## ChatMessage
|
||||
(Just $ SharedMsgId "\1\2\3\4")
|
||||
( XMsgNew $
|
||||
MCQuote
|
||||
( QuotedMsg
|
||||
(MsgRef (Just $ SharedMsgId "\5\6\7\8") (systemToUTCTime $ MkSystemTime 1 1) True Nothing)
|
||||
$ MCText "hello there!"
|
||||
)
|
||||
(MCText "hello to you too")
|
||||
)
|
||||
it "x.msg.new" $
|
||||
"{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}"
|
||||
##==## (ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew . MCForward $ MCText "hello"))
|
||||
##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew . MCForward $ MCText "hello")
|
||||
it "x.file" $
|
||||
"{\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23MCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
|
||||
#==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = testConnReq}
|
||||
#==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileConnReq = ACR SCMInvitation testConnReq}
|
||||
it "x.file.acpt" $ "{\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}" #==# XFileAcpt "photo.jpg"
|
||||
it "x.info" $ "{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" #==# XInfo testProfile
|
||||
it "x.info" $ "{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\"}}}" #==# XInfo Profile {displayName = "alice", fullName = "", image = Nothing}
|
||||
|
||||
Reference in New Issue
Block a user