mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge pull request #1698 from simplex-chat/users
support multiple user profiles
This commit is contained in:
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
- sqlcipher
|
||||
- users
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
|
||||
@@ -35,6 +35,7 @@ import kotlin.time.*
|
||||
class ChatModel(val controller: ChatController) {
|
||||
val onboardingStage = mutableStateOf<OnboardingStage?>(null)
|
||||
val currentUser = mutableStateOf<User?>(null)
|
||||
val users = mutableStateListOf<UserInfo>()
|
||||
val userCreated = mutableStateOf<Boolean?>(null)
|
||||
val chatRunning = mutableStateOf<Boolean?>(null)
|
||||
val chatDbChanged = mutableStateOf<Boolean>(false)
|
||||
@@ -42,6 +43,8 @@ class ChatModel(val controller: ChatController) {
|
||||
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
|
||||
val chatDbDeleted = mutableStateOf(false)
|
||||
val chats = mutableStateListOf<Chat>()
|
||||
// map of connections network statuses, key is agent connection id
|
||||
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
||||
|
||||
// current chat
|
||||
val chatId = mutableStateOf<String?>(null)
|
||||
@@ -87,13 +90,6 @@ class ChatModel(val controller: ChatController) {
|
||||
val filesToDelete = mutableSetOf<File>()
|
||||
val simplexLinkMode = mutableStateOf(controller.appPrefs.simplexLinkMode.get())
|
||||
|
||||
fun updateUserProfile(profile: LocalProfile) {
|
||||
val user = currentUser.value
|
||||
if (user != null) {
|
||||
currentUser.value = user.copy(profile = profile)
|
||||
}
|
||||
}
|
||||
|
||||
fun hasChat(id: String): Boolean = chats.firstOrNull { it.id == id } != null
|
||||
fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id }
|
||||
fun getContactChat(contactId: Long): Chat? = chats.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
|
||||
@@ -120,17 +116,8 @@ class ChatModel(val controller: ChatController) {
|
||||
}
|
||||
|
||||
fun updateChats(newChats: List<Chat>) {
|
||||
val mergedChats = arrayListOf<Chat>()
|
||||
for (newChat in newChats) {
|
||||
val i = getChatIndex(newChat.chatInfo.id)
|
||||
if (i >= 0) {
|
||||
mergedChats.add(newChat.copy(serverInfo = chats[i].serverInfo))
|
||||
} else {
|
||||
mergedChats.add(newChat)
|
||||
}
|
||||
}
|
||||
chats.clear()
|
||||
chats.addAll(mergedChats)
|
||||
chats.addAll(newChats)
|
||||
|
||||
val cId = chatId.value
|
||||
// If chat is null, it was deleted in background after apiGetChats call
|
||||
@@ -139,14 +126,6 @@ class ChatModel(val controller: ChatController) {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNetworkStatus(id: ChatId, status: Chat.NetworkStatus) {
|
||||
val i = getChatIndex(id)
|
||||
if (i >= 0) {
|
||||
val chat = chats[i]
|
||||
chats[i] = chat.copy(serverInfo = chat.serverInfo.copy(networkStatus = status))
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceChat(id: String, chat: Chat) {
|
||||
val i = getChatIndex(id)
|
||||
if (i >= 0) {
|
||||
@@ -168,6 +147,7 @@ class ChatModel(val controller: ChatController) {
|
||||
chatStats =
|
||||
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
|
||||
val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId
|
||||
increaseUnreadCounter(currentUser.value!!)
|
||||
chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, minUnreadItemId = minUnreadId)
|
||||
}
|
||||
else
|
||||
@@ -256,6 +236,7 @@ class ChatModel(val controller: ChatController) {
|
||||
// clear preview
|
||||
val i = getChatIndex(cInfo.id)
|
||||
if (i >= 0) {
|
||||
decreaseUnreadCounter(currentUser.value!!, chats[i].chatStats.unreadCount)
|
||||
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
|
||||
}
|
||||
// clear current chat
|
||||
@@ -264,6 +245,19 @@ class ChatModel(val controller: ChatController) {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCurrentUser(newProfile: Profile, preferences: FullChatPreferences? = null) {
|
||||
val current = currentUser.value ?: return
|
||||
val updated = current.copy(
|
||||
profile = newProfile.toLocalProfile(current.profile.profileId),
|
||||
fullPreferences = preferences ?: current.fullPreferences
|
||||
)
|
||||
val indexInUsers = users.indexOfFirst { it.user.userId == current.userId }
|
||||
if (indexInUsers != -1) {
|
||||
users[indexInUsers] = UserInfo(updated, users[indexInUsers].unreadCount)
|
||||
}
|
||||
currentUser.value = updated
|
||||
}
|
||||
|
||||
suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
|
||||
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -286,9 +280,11 @@ class ChatModel(val controller: ChatController) {
|
||||
val chat = chats[chatIdx]
|
||||
val lastId = chat.chatItems.lastOrNull()?.id
|
||||
if (lastId != null) {
|
||||
val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0
|
||||
decreaseUnreadCounter(currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIdx] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0,
|
||||
unreadCount = unreadCount,
|
||||
// Can't use minUnreadItemId currently since chat items can have unread items between read items
|
||||
//minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1
|
||||
)
|
||||
@@ -324,13 +320,30 @@ class ChatModel(val controller: ChatController) {
|
||||
if (chatIndex == -1) return
|
||||
|
||||
val chat = chats[chatIndex]
|
||||
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
|
||||
decreaseUnreadCounter(currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIndex] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0),
|
||||
unreadCount = unreadCount,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun increaseUnreadCounter(user: User) {
|
||||
changeUnreadCounter(user, 1)
|
||||
}
|
||||
|
||||
fun decreaseUnreadCounter(user: User, by: Int = 1) {
|
||||
changeUnreadCounter(user, -by)
|
||||
}
|
||||
|
||||
private fun changeUnreadCounter(user: User, by: Int) {
|
||||
val i = users.indexOfFirst { it.user.userId == user.userId }
|
||||
if (i != -1) {
|
||||
users[i] = UserInfo(user, users[i].unreadCount + by)
|
||||
}
|
||||
}
|
||||
|
||||
// func popChat(_ id: String) {
|
||||
// if let i = getChatIndex(id) {
|
||||
// popChat_(i)
|
||||
@@ -375,6 +388,13 @@ class ChatModel(val controller: ChatController) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) {
|
||||
networkStatuses[contact.activeConn.agentConnId] = status
|
||||
}
|
||||
|
||||
fun contactNetworkStatus(contact: Contact): NetworkStatus =
|
||||
networkStatuses[contact.activeConn.agentConnId] ?: NetworkStatus.Unknown()
|
||||
}
|
||||
|
||||
enum class ChatType(val type: String) {
|
||||
@@ -410,6 +430,19 @@ data class User(
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UserInfo(
|
||||
val user: User,
|
||||
val unreadCount: Int
|
||||
) {
|
||||
companion object {
|
||||
val sampleData = UserInfo(
|
||||
user = User.sampleData,
|
||||
unreadCount = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
typealias ChatId = String
|
||||
|
||||
interface NamedChat {
|
||||
@@ -441,37 +474,12 @@ data class Chat (
|
||||
val chatInfo: ChatInfo,
|
||||
val chatItems: List<ChatItem>,
|
||||
val chatStats: ChatStats = ChatStats(),
|
||||
val serverInfo: ServerInfo = ServerInfo(NetworkStatus.Unknown())
|
||||
) {
|
||||
val id: String get() = chatInfo.id
|
||||
|
||||
@Serializable
|
||||
data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false)
|
||||
|
||||
@Serializable
|
||||
data class ServerInfo(val networkStatus: NetworkStatus)
|
||||
|
||||
@Serializable
|
||||
sealed class NetworkStatus {
|
||||
val statusString: String get() =
|
||||
when (this) {
|
||||
is Connected -> generalGetString(R.string.server_connected)
|
||||
is Error -> generalGetString(R.string.server_error)
|
||||
else -> generalGetString(R.string.server_connecting)
|
||||
}
|
||||
val statusExplanation: String get() =
|
||||
when (this) {
|
||||
is Connected -> generalGetString(R.string.connected_to_server_to_receive_messages_from_contact)
|
||||
is Error -> String.format(generalGetString(R.string.trying_to_connect_to_server_to_receive_messages_with_error), error)
|
||||
else -> generalGetString(R.string.trying_to_connect_to_server_to_receive_messages)
|
||||
}
|
||||
|
||||
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
|
||||
@Serializable @SerialName("connected") class Connected: NetworkStatus()
|
||||
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
|
||||
@Serializable @SerialName("error") class Error(val error: String): NetworkStatus()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val sampleData = Chat(
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
@@ -605,6 +613,27 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class NetworkStatus {
|
||||
val statusString: String get() =
|
||||
when (this) {
|
||||
is Connected -> generalGetString(R.string.server_connected)
|
||||
is Error -> generalGetString(R.string.server_error)
|
||||
else -> generalGetString(R.string.server_connecting)
|
||||
}
|
||||
val statusExplanation: String get() =
|
||||
when (this) {
|
||||
is Connected -> generalGetString(R.string.connected_to_server_to_receive_messages_from_contact)
|
||||
is Error -> String.format(generalGetString(R.string.trying_to_connect_to_server_to_receive_messages_with_error), error)
|
||||
else -> generalGetString(R.string.trying_to_connect_to_server_to_receive_messages)
|
||||
}
|
||||
|
||||
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
|
||||
@Serializable @SerialName("connected") class Connected: NetworkStatus()
|
||||
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
|
||||
@Serializable @SerialName("error") class Error(val error: String): NetworkStatus()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Contact(
|
||||
val contactId: Long,
|
||||
@@ -675,6 +704,8 @@ data class Contact(
|
||||
@Serializable
|
||||
class ContactRef(
|
||||
val contactId: Long,
|
||||
val agentConnId: String,
|
||||
val connId: Long,
|
||||
var localDisplayName: String
|
||||
) {
|
||||
val id: ChatId get() = "@$contactId"
|
||||
@@ -689,6 +720,7 @@ class ContactSubStatus(
|
||||
@Serializable
|
||||
data class Connection(
|
||||
val connId: Long,
|
||||
val agentConnId: String,
|
||||
val connStatus: ConnStatus,
|
||||
val connLevel: Int,
|
||||
val viaGroupLink: Boolean,
|
||||
@@ -697,7 +729,7 @@ data class Connection(
|
||||
) {
|
||||
val id: ChatId get() = ":$connId"
|
||||
companion object {
|
||||
val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null)
|
||||
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,8 +77,9 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyContactRequestReceived(cInfo: ChatInfo.ContactRequest) {
|
||||
fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) {
|
||||
notifyMessageReceived(
|
||||
user = user,
|
||||
chatId = cInfo.id,
|
||||
displayName = cInfo.displayName,
|
||||
msgText = generalGetString(R.string.notification_new_contact_request),
|
||||
@@ -87,21 +88,22 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
)
|
||||
}
|
||||
|
||||
fun notifyContactConnected(contact: Contact) {
|
||||
fun notifyContactConnected(user: User, contact: Contact) {
|
||||
notifyMessageReceived(
|
||||
user = user,
|
||||
chatId = contact.id,
|
||||
displayName = contact.displayName,
|
||||
msgText = generalGetString(R.string.notification_contact_connected)
|
||||
)
|
||||
}
|
||||
|
||||
fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) {
|
||||
fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
if (!cInfo.ntfsEnabled) return
|
||||
|
||||
notifyMessageReceived(chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
|
||||
notifyMessageReceived(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
|
||||
}
|
||||
|
||||
fun notifyMessageReceived(chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) {
|
||||
fun notifyMessageReceived(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) {
|
||||
Log.d(TAG, "notifyMessageReceived $chatId")
|
||||
val now = Clock.System.now().toEpochMilliseconds()
|
||||
val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -102,7 +102,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
|
||||
val prefPerformLA = chatModel.controller.appPrefs.performLA.get()
|
||||
val s = composeState.value.message
|
||||
if (s.startsWith("/sql") && (!prefPerformLA || !developerTools)) {
|
||||
val resp = CR.ChatCmdError(ChatError.ChatErrorChat(ChatErrorType.СommandError("Failed reading: empty")))
|
||||
val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.СommandError("Failed reading: empty")))
|
||||
chatModel.terminalItems.add(TerminalItem.cmd(CC.Console(s)))
|
||||
chatModel.terminalItems.add(TerminalItem.resp(resp))
|
||||
composeState.value = ComposeState(useLinkPreviews = false)
|
||||
|
||||
@@ -18,7 +18,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||
controller.ntfManager.notifyCallInvitation(invitation)
|
||||
} else {
|
||||
val contact = invitation.contact
|
||||
controller.ntfManager.notifyMessageReceived(chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
|
||||
controller.ntfManager.notifyMessageReceived(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +217,7 @@ fun PreviewIncomingCallLockScreenAlert() {
|
||||
.fillMaxSize()) {
|
||||
IncomingCallLockScreenAlertLayout(
|
||||
invitation = RcvCallInvitation(
|
||||
user = User.sampleData,
|
||||
contact = Contact.sampleData,
|
||||
callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)),
|
||||
sharedKey = null,
|
||||
|
||||
@@ -17,8 +17,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.Contact
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.usersettings.ProfilePreview
|
||||
import kotlinx.datetime.Clock
|
||||
@@ -101,6 +100,7 @@ fun PreviewIncomingCallAlertLayout() {
|
||||
SimpleXTheme {
|
||||
IncomingCallAlertLayout(
|
||||
invitation = RcvCallInvitation(
|
||||
user = User.sampleData,
|
||||
contact = Contact.sampleData,
|
||||
callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)),
|
||||
sharedKey = null,
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.Contact
|
||||
import chat.simplex.app.model.User
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -93,7 +94,7 @@ sealed class WCallResponse {
|
||||
@Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
|
||||
@Serializable class WebRTCExtraInfo(val rtcIceCandidates: String)
|
||||
@Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
|
||||
@Serializable class RcvCallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
|
||||
@Serializable class RcvCallInvitation(val user: User, val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
|
||||
val callTypeText: String get() = generalGetString(when(callType.media) {
|
||||
CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
|
||||
CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
|
||||
|
||||
@@ -54,10 +54,14 @@ fun ChatInfoView(
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null) {
|
||||
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) {
|
||||
mutableStateOf(chatModel.contactNetworkStatus(contact))
|
||||
}
|
||||
ChatInfoLayout(
|
||||
chat,
|
||||
contact,
|
||||
connStats,
|
||||
contactNetworkStatus.value,
|
||||
customUserProfile,
|
||||
localAlias,
|
||||
connectionCode,
|
||||
@@ -149,6 +153,7 @@ fun ChatInfoLayout(
|
||||
chat: Chat,
|
||||
contact: Contact,
|
||||
connStats: ConnectionStats?,
|
||||
contactNetworkStatus: NetworkStatus,
|
||||
customUserProfile: Profile?,
|
||||
localAlias: String,
|
||||
connectionCode: String?,
|
||||
@@ -200,9 +205,9 @@ fun ChatInfoLayout(
|
||||
SectionItemView({
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.network_status),
|
||||
chat.serverInfo.networkStatus.statusExplanation
|
||||
contactNetworkStatus.statusExplanation
|
||||
)}) {
|
||||
NetworkStatusRow(chat.serverInfo.networkStatus)
|
||||
NetworkStatusRow(contactNetworkStatus)
|
||||
}
|
||||
val rcvServers = connStats.rcvServers
|
||||
if (rcvServers != null && rcvServers.isNotEmpty()) {
|
||||
@@ -314,7 +319,7 @@ fun LocalAliasEditor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
|
||||
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
||||
Row(
|
||||
Modifier.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -346,14 +351,14 @@ private fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServerImage(networkStatus: Chat.NetworkStatus) {
|
||||
private fun ServerImage(networkStatus: NetworkStatus) {
|
||||
Box(Modifier.size(18.dp)) {
|
||||
when (networkStatus) {
|
||||
is Chat.NetworkStatus.Connected ->
|
||||
is NetworkStatus.Connected ->
|
||||
Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_server_status_connected), tint = MaterialTheme.colors.primaryVariant)
|
||||
is Chat.NetworkStatus.Disconnected ->
|
||||
is NetworkStatus.Disconnected ->
|
||||
Icon(Icons.Filled.Pending, stringResource(R.string.icon_descr_server_status_disconnected), tint = HighOrLowlight)
|
||||
is Chat.NetworkStatus.Error ->
|
||||
is NetworkStatus.Error ->
|
||||
Icon(Icons.Filled.Error, stringResource(R.string.icon_descr_server_status_error), tint = HighOrLowlight)
|
||||
else -> Icon(Icons.Outlined.Circle, stringResource(R.string.icon_descr_server_status_pending), tint = HighOrLowlight)
|
||||
}
|
||||
@@ -446,14 +451,14 @@ fun PreviewChatInfoLayout() {
|
||||
ChatInfoLayout(
|
||||
chat = Chat(
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
chatItems = arrayListOf(),
|
||||
serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT"))
|
||||
chatItems = arrayListOf()
|
||||
),
|
||||
Contact.sampleData,
|
||||
localAlias = "",
|
||||
connectionCode = "123",
|
||||
developerTools = false,
|
||||
connStats = null,
|
||||
contactNetworkStatus = NetworkStatus.Connected(),
|
||||
onLocalAliasChanged = {},
|
||||
customUserProfile = null,
|
||||
openPreferences = {},
|
||||
|
||||
+1
-2
@@ -427,8 +427,7 @@ fun PreviewGroupChatInfoLayout() {
|
||||
GroupChatInfoLayout(
|
||||
chat = Chat(
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
chatItems = arrayListOf(),
|
||||
serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT"))
|
||||
chatItems = arrayListOf()
|
||||
),
|
||||
groupInfo = GroupInfo.sampleData,
|
||||
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
||||
|
||||
+2
-4
@@ -66,11 +66,9 @@ fun GroupMemberInfoView(
|
||||
withApi {
|
||||
val c = chatModel.controller.apiGetChat(ChatType.Direct, it)
|
||||
if (c != null) {
|
||||
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend
|
||||
val newChat = c.copy(serverInfo = Chat.ServerInfo(networkStatus = Chat.NetworkStatus.Connected()))
|
||||
chatModel.addChat(newChat)
|
||||
chatModel.addChat(c)
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatId.value = newChat.id
|
||||
chatModel.chatId.value = c.id
|
||||
closeAll()
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -44,17 +44,19 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
delay(500L)
|
||||
}
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
is ChatInfo.Direct -> {
|
||||
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped, linkMode) },
|
||||
chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) },
|
||||
click = { directChatAction(chat.chatInfo, chatModel) },
|
||||
dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) },
|
||||
showMenu,
|
||||
stopped
|
||||
)
|
||||
}
|
||||
is ChatInfo.Group ->
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped, linkMode) },
|
||||
chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) },
|
||||
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
|
||||
dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) },
|
||||
showMenu,
|
||||
@@ -627,6 +629,7 @@ fun PreviewChatListNavLinkDirect() {
|
||||
),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
stopped = false,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION
|
||||
)
|
||||
@@ -665,6 +668,7 @@ fun PreviewChatListNavLinkGroup() {
|
||||
),
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
stopped = false,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -13,16 +14,15 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.connectIfOpenedViaUri
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
@@ -37,13 +37,14 @@ import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||
val newChatSheetState by rememberSaveable(stateSaver = NewChatSheetState.saver()) { mutableStateOf(MutableStateFlow(NewChatSheetState.GONE)) }
|
||||
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val showNewChatSheet = {
|
||||
newChatSheetState.value = NewChatSheetState.VISIBLE
|
||||
newChatSheetState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
val hideNewChatSheet: (animated: Boolean) -> Unit = { animated ->
|
||||
if (animated) newChatSheetState.value = NewChatSheetState.HIDING
|
||||
else newChatSheetState.value = NewChatSheetState.GONE
|
||||
if (animated) newChatSheetState.value = AnimatedViewState.HIDING
|
||||
else newChatSheetState.value = AnimatedViewState.GONE
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
if (shouldShowWhatsNew(chatModel)) {
|
||||
@@ -63,8 +64,9 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
}
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val scope = rememberCoroutineScope()
|
||||
Scaffold(
|
||||
topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, stopped) { searchInList = it.trim() } },
|
||||
topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } },
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = { SettingsView(chatModel, setPerformLA) },
|
||||
floatingActionButton = {
|
||||
@@ -111,6 +113,9 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
if (searchInList.isEmpty()) {
|
||||
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
||||
}
|
||||
UserPicker(chatModel, userPickerState) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -156,7 +161,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
|
||||
private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
|
||||
var showSearch by rememberSaveable { mutableStateOf(false) }
|
||||
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
|
||||
if (showSearch) {
|
||||
@@ -189,10 +194,23 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
|
||||
val scope = rememberCoroutineScope()
|
||||
DefaultTopAppBar(
|
||||
navigationButton = {
|
||||
if (showSearch)
|
||||
if (showSearch) {
|
||||
NavigationButtonBack(hideSearchOnBack)
|
||||
else
|
||||
} else if (chatModel.users.isEmpty()) {
|
||||
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
|
||||
} else {
|
||||
val users by remember { derivedStateOf { chatModel.users.toList() } }
|
||||
val allRead = users
|
||||
.filter { !it.user.activeUser }
|
||||
.all { u -> u.unreadCount == 0 }
|
||||
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
||||
if (users.size == 1) {
|
||||
scope.launch { drawerState.open() }
|
||||
} else {
|
||||
userPickerState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -219,6 +237,36 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
|
||||
Divider(Modifier.padding(top = AppBarHeight))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
|
||||
IconButton(onClick = onButtonClicked) {
|
||||
Box {
|
||||
ProfileImage(
|
||||
image = image,
|
||||
size = 37.dp
|
||||
)
|
||||
if (!allRead) {
|
||||
unreadBadge()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.unreadBadge(text: String? = "") {
|
||||
Text(
|
||||
text ?: "",
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 6.sp,
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.primary, shape = CircleShape)
|
||||
.badgeLayout()
|
||||
.padding(horizontal = 3.dp)
|
||||
.padding(vertical = 1.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -25,7 +25,14 @@ import chat.simplex.app.views.chat.item.MarkdownText
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, stopped: Boolean, linkMode: SimplexLinkMode) {
|
||||
fun ChatPreviewView(
|
||||
chat: Chat,
|
||||
chatModelIncognito: Boolean,
|
||||
currentUserProfileDisplayName: String?,
|
||||
contactNetworkStatus: NetworkStatus?,
|
||||
stopped: Boolean,
|
||||
linkMode: SimplexLinkMode
|
||||
) {
|
||||
val cInfo = chat.chatInfo
|
||||
|
||||
@Composable
|
||||
@@ -187,7 +194,9 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
|
||||
Modifier.padding(top = 52.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ChatStatusImage(chat)
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
ChatStatusImage(chat, contactNetworkStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,10 +219,9 @@ fun unreadCountStr(n: Int): String {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatStatusImage(chat: Chat) {
|
||||
val s = chat.serverInfo.networkStatus
|
||||
val descr = s.statusString
|
||||
if (s is Chat.NetworkStatus.Error) {
|
||||
fun ChatStatusImage(chat: Chat, s: NetworkStatus?) {
|
||||
val descr = s?.statusString
|
||||
if (s is NetworkStatus.Error) {
|
||||
Icon(
|
||||
Icons.Outlined.ErrorOutline,
|
||||
contentDescription = descr,
|
||||
@@ -221,7 +229,7 @@ fun ChatStatusImage(chat: Chat) {
|
||||
modifier = Modifier
|
||||
.size(19.dp)
|
||||
)
|
||||
} else if (s !is Chat.NetworkStatus.Connected) {
|
||||
} else if (s !is NetworkStatus.Connected) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
@@ -241,6 +249,6 @@ fun ChatStatusImage(chat: Chat) {
|
||||
@Composable
|
||||
fun PreviewChatPreviewView() {
|
||||
SimpleXTheme {
|
||||
ChatPreviewView(Chat.sampleData, false, "", stopped = false, linkMode = SimplexLinkMode.DESCRIPTION)
|
||||
ChatPreviewView(Chat.sampleData, false, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package chat.simplex.app.views.chatlist
|
||||
|
||||
import SectionItemViewSpaceBetween
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.UserInfo
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, openSettings: () -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var newChat by remember { mutableStateOf(userPickerState.value) }
|
||||
val users by remember { derivedStateOf { chatModel.users.sortedByDescending { it.user.activeUser } } }
|
||||
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
|
||||
var progressIndicator by remember { mutableStateOf(false) }
|
||||
if (progressIndicator) {
|
||||
Box(
|
||||
Modifier.fillMaxSize().clickable(enabled = false, onClick = {}),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ProgressIndicator()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
userPickerState.collect {
|
||||
newChat = it
|
||||
launch {
|
||||
animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec())
|
||||
if (newChat.isHiding()) userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { newChat.isVisible() }
|
||||
.distinctUntilChanged()
|
||||
.filter { it }
|
||||
.collect {
|
||||
try {
|
||||
val updatedUsers = chatModel.controller.listUsers().sortedByDescending { it.user.activeUser }
|
||||
var same = users.size == updatedUsers.size
|
||||
if (same) {
|
||||
for (i in 0 until minOf(users.size, updatedUsers.size)) {
|
||||
val prev = updatedUsers[i].user
|
||||
val next = users[i].user
|
||||
if (prev.userId != next.userId || prev.activeUser != next.activeUser || prev.chatViewName != next.chatViewName || prev.image != next.image) {
|
||||
same = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!same) {
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(updatedUsers)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error updating users ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() }
|
||||
val maxWidth = with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp * density }
|
||||
Box(Modifier
|
||||
.fillMaxSize()
|
||||
.offset { IntOffset(if (newChat.isGone()) -maxWidth.roundToInt() else xOffset, 0) }
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING })
|
||||
.padding(bottom = 10.dp, top = 10.dp)
|
||||
.graphicsLayer {
|
||||
alpha = animatedFloat.value
|
||||
translationY = (animatedFloat.value - 1) * xOffset
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.widthIn(min = 220.dp)
|
||||
.width(IntrinsicSize.Min)
|
||||
.height(IntrinsicSize.Min)
|
||||
.shadow(8.dp, MaterialTheme.shapes.medium, clip = false)
|
||||
.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
|
||||
users.forEachIndexed { i, u ->
|
||||
UserProfilePickerItem(u) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
if (!u.user.activeUser) {
|
||||
chatModel.chats.clear()
|
||||
scope.launch {
|
||||
val job = launch {
|
||||
delay(500)
|
||||
progressIndicator = true
|
||||
}
|
||||
chatModel.controller.changeActiveUser(u.user.userId)
|
||||
job.cancel()
|
||||
progressIndicator = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i != users.lastIndex) {
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
SettingsPickerItem {
|
||||
openSettings()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserProfilePickerItem(u: UserInfo, onClick: () -> Unit) {
|
||||
SectionItemViewSpaceBetween(onClick, padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ProfileImage(
|
||||
image = u.user.image,
|
||||
size = 54.dp
|
||||
)
|
||||
Text(
|
||||
u.user.chatViewName,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, end = 8.dp)
|
||||
)
|
||||
}
|
||||
if (u.user.activeUser) {
|
||||
Icon(Icons.Filled.Done, null, Modifier.size(20.dp), tint = MaterialTheme.colors.primary)
|
||||
} else if (u.unreadCount > 0) {
|
||||
Text(
|
||||
unreadCountStr(u.unreadCount),
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.primary, shape = CircleShape)
|
||||
.sizeIn(minWidth = 20.dp, minHeight = 20.dp)
|
||||
.padding(horizontal = 3.dp)
|
||||
.padding(vertical = 1.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
)
|
||||
} else {
|
||||
Box(Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsPickerItem(onClick: () -> Unit) {
|
||||
SectionItemViewSpaceBetween(onClick, minHeight = 68.dp) {
|
||||
val text = generalGetString(R.string.settings_section_title_settings).lowercase().capitalize(Locale.current)
|
||||
Text(
|
||||
text,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
)
|
||||
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressIndicator() {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -11,7 +10,7 @@ sealed class SharedContent {
|
||||
data class File(val text: String, val uri: Uri): SharedContent()
|
||||
}
|
||||
|
||||
enum class NewChatSheetState {
|
||||
enum class AnimatedViewState {
|
||||
VISIBLE, HIDING, GONE;
|
||||
fun isVisible(): Boolean {
|
||||
return this == VISIBLE
|
||||
@@ -23,7 +22,7 @@ enum class NewChatSheetState {
|
||||
return this == GONE
|
||||
}
|
||||
companion object {
|
||||
fun saver(): Saver<MutableStateFlow<NewChatSheetState>, *> = Saver(
|
||||
fun saver(): Saver<MutableStateFlow<AnimatedViewState>, *> = Saver(
|
||||
save = { it.value.toString() },
|
||||
restore = {
|
||||
MutableStateFlow(valueOf(it))
|
||||
|
||||
@@ -50,8 +50,8 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
|
||||
rec.setAudioEncodingBitRate(16000)
|
||||
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
|
||||
rec.setMaxFileSize(recordedBytesLimit)
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val path = getAppFilePath(SimplexApp.context, uniqueCombine(SimplexApp.context, getAppFilePath(SimplexApp.context, "voice_${timestamp}.m4a")))
|
||||
val fileToSave = generateNewFileName(SimplexApp.context, "voice", "m4a")
|
||||
val path = getAppFilePath(SimplexApp.context, fileToSave)
|
||||
filePath = path
|
||||
rec.setOutputFile(path)
|
||||
rec.prepare()
|
||||
|
||||
@@ -331,8 +331,7 @@ fun saveImage(context: Context, image: Bitmap): String? {
|
||||
return try {
|
||||
val ext = if (image.hasAlpha()) "png" else "jpg"
|
||||
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val fileToSave = uniqueCombine(context, "IMG_${timestamp}.$ext")
|
||||
val fileToSave = generateNewFileName(context, "IMG", ext)
|
||||
val file = File(getAppFilePath(context, fileToSave))
|
||||
val output = FileOutputStream(file)
|
||||
dataResized.writeTo(output)
|
||||
@@ -355,8 +354,7 @@ fun saveAnimImage(context: Context, uri: Uri): String? {
|
||||
}
|
||||
// Just in case the image has a strange extension
|
||||
if (ext.length < 3 || ext.length > 4) ext = "gif"
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val fileToSave = uniqueCombine(context, "IMG_${timestamp}.$ext")
|
||||
val fileToSave = generateNewFileName(context, "IMG", ext)
|
||||
val file = File(getAppFilePath(context, fileToSave))
|
||||
val output = FileOutputStream(file)
|
||||
context.contentResolver.openInputStream(uri)!!.use { input ->
|
||||
@@ -390,15 +388,23 @@ fun saveFileFromUri(context: Context, uri: Uri): String? {
|
||||
}
|
||||
}
|
||||
|
||||
fun generateNewFileName(context: Context, prefix: String, ext: String): String {
|
||||
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
|
||||
sdf.timeZone = TimeZone.getTimeZone("GMT")
|
||||
val timestamp = sdf.format(Date())
|
||||
return uniqueCombine(context, "${prefix}_$timestamp.$ext")
|
||||
}
|
||||
|
||||
fun uniqueCombine(context: Context, fileName: String): String {
|
||||
fun tryCombine(fileName: String, n: Int): String {
|
||||
val name = File(fileName).nameWithoutExtension
|
||||
val ext = File(fileName).extension
|
||||
val orig = File(fileName)
|
||||
val name = orig.nameWithoutExtension
|
||||
val ext = orig.extension
|
||||
fun tryCombine(n: Int): String {
|
||||
val suffix = if (n == 0) "" else "_$n"
|
||||
val f = "$name$suffix.$ext"
|
||||
return if (File(getAppFilePath(context, f)).exists()) tryCombine(fileName, n + 1) else f
|
||||
return if (File(getAppFilePath(context, f)).exists()) tryCombine(n + 1) else f
|
||||
}
|
||||
return tryCombine(fileName, 0)
|
||||
return tryCombine(0)
|
||||
}
|
||||
|
||||
fun formatBytes(bytes: Long): String {
|
||||
|
||||
@@ -37,7 +37,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<NewChatSheetState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
|
||||
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
|
||||
if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) }
|
||||
NewChatSheetLayout(
|
||||
newChatSheetState,
|
||||
@@ -63,7 +63,7 @@ private val icons = listOf(Icons.Outlined.AddLink, Icons.Outlined.QrCode, Icons.
|
||||
|
||||
@Composable
|
||||
private fun NewChatSheetLayout(
|
||||
newChatSheetState: StateFlow<NewChatSheetState>,
|
||||
newChatSheetState: StateFlow<AnimatedViewState>,
|
||||
stopped: Boolean,
|
||||
addContact: () -> Unit,
|
||||
connectViaLink: () -> Unit,
|
||||
@@ -216,7 +216,7 @@ fun ActionButton(
|
||||
private fun PreviewNewChatSheet() {
|
||||
SimpleXTheme {
|
||||
NewChatSheetLayout(
|
||||
MutableStateFlow(NewChatSheetState.VISIBLE),
|
||||
MutableStateFlow(AnimatedViewState.VISIBLE),
|
||||
stopped = false,
|
||||
addContact = {},
|
||||
connectViaLink = {},
|
||||
|
||||
+3
@@ -60,6 +60,9 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
}
|
||||
return NetCfg(
|
||||
socksProxy = currentCfg.value.socksProxy,
|
||||
hostMode = currentCfg.value.hostMode,
|
||||
requiredHostMode = currentCfg.value.requiredHostMode,
|
||||
sessionMode = currentCfg.value.sessionMode,
|
||||
tcpConnectTimeout = networkTCPConnectTimeout.value,
|
||||
tcpTimeout = networkTCPTimeout.value,
|
||||
tcpKeepAlive = tcpKeepAlive,
|
||||
|
||||
+77
-6
@@ -31,6 +31,7 @@ fun NetworkAndServersView(
|
||||
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
|
||||
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
chatModel.userSMPServersUnsaved.value = null
|
||||
@@ -40,6 +41,7 @@ fun NetworkAndServersView(
|
||||
developerTools = developerTools,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = onionHosts,
|
||||
sessionMode = sessionMode,
|
||||
showModal = showModal,
|
||||
showSettingsModal = showSettingsModal,
|
||||
toggleSocksProxy = { enable ->
|
||||
@@ -82,9 +84,13 @@ fun NetworkAndServersView(
|
||||
OnionHosts.PREFER -> generalGetString(R.string.network_use_onion_hosts_prefer_desc_in_alert)
|
||||
OnionHosts.REQUIRED -> generalGetString(R.string.network_use_onion_hosts_required_desc_in_alert)
|
||||
}
|
||||
updateOnionHostsDialog(startsWith, onDismiss = {
|
||||
onionHosts.value = prevValue
|
||||
}) {
|
||||
updateNetworkSettingsDialog(
|
||||
title = generalGetString(R.string.update_onion_hosts_settings_question),
|
||||
startsWith,
|
||||
onDismiss = {
|
||||
onionHosts.value = prevValue
|
||||
}
|
||||
) {
|
||||
withApi {
|
||||
val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it)
|
||||
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
|
||||
@@ -96,6 +102,31 @@ fun NetworkAndServersView(
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateSessionMode = {
|
||||
if (sessionMode.value == it) return@NetworkAndServersLayout
|
||||
val prevValue = sessionMode.value
|
||||
sessionMode.value = it
|
||||
val startsWith = when (it) {
|
||||
TransportSessionMode.User -> generalGetString(R.string.network_session_mode_user_description)
|
||||
TransportSessionMode.Entity -> generalGetString(R.string.network_session_mode_entity_description)
|
||||
}
|
||||
updateNetworkSettingsDialog(
|
||||
title = generalGetString(R.string.update_network_session_mode_question),
|
||||
startsWith,
|
||||
onDismiss = { sessionMode.value = prevValue }
|
||||
) {
|
||||
withApi {
|
||||
val newCfg = chatModel.controller.getNetCfg().copy(sessionMode = it)
|
||||
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
|
||||
if (res) {
|
||||
chatModel.controller.setNetCfg(newCfg)
|
||||
sessionMode.value = it
|
||||
} else {
|
||||
sessionMode.value = prevValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -104,10 +135,12 @@ fun NetworkAndServersView(
|
||||
developerTools: Boolean,
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -124,6 +157,10 @@ fun NetworkAndServersView(
|
||||
SectionDivider()
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
|
||||
SectionDivider()
|
||||
if (developerTools) {
|
||||
SessionModePicker(sessionMode, showSettingsModal, updateSessionMode)
|
||||
SectionDivider()
|
||||
}
|
||||
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
@@ -183,7 +220,6 @@ private fun UseOnionHosts(
|
||||
}
|
||||
}
|
||||
val onSelected = showModal {
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -203,14 +239,47 @@ private fun UseOnionHosts(
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateOnionHostsDialog(
|
||||
@Composable
|
||||
private fun SessionModePicker(
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
) {
|
||||
val values = remember {
|
||||
TransportSessionMode.values().map {
|
||||
when (it) {
|
||||
TransportSessionMode.User -> ValueTitleDesc(TransportSessionMode.User, generalGetString(R.string.network_session_mode_user), generalGetString(R.string.network_session_mode_user_description))
|
||||
TransportSessionMode.Entity -> ValueTitleDesc(TransportSessionMode.Entity, generalGetString(R.string.network_session_mode_entity), generalGetString(R.string.network_session_mode_entity_description))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionItemWithValue(
|
||||
generalGetString(R.string.network_session_mode_transport_isolation),
|
||||
sessionMode,
|
||||
values,
|
||||
icon = Icons.Outlined.SafetyDivider,
|
||||
onSelected = showModal {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.network_session_mode_transport_isolation))
|
||||
SectionViewSelectable(null, sessionMode, values, updateSessionMode)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateNetworkSettingsDialog(
|
||||
title: String,
|
||||
startsWith: String = "",
|
||||
message: String = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.update_onion_hosts_settings_question),
|
||||
title = title,
|
||||
text = startsWith + "\n\n" + message,
|
||||
confirmText = generalGetString(R.string.update_network_settings_confirmation),
|
||||
onDismiss = onDismiss,
|
||||
@@ -230,7 +299,9 @@ fun PreviewNetworkAndServersLayout() {
|
||||
showSettingsModal = { {} },
|
||||
toggleSocksProxy = {},
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
|
||||
useOnion = {},
|
||||
updateSessionMode = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,8 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
|
||||
val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences())
|
||||
val updatedProfile = m.controller.apiUpdateProfile(newProfile)
|
||||
if (updatedProfile != null) {
|
||||
val updatedUser = user.copy(
|
||||
profile = updatedProfile.toLocalProfile(user.profile.profileId),
|
||||
fullPreferences = preferences
|
||||
)
|
||||
m.updateCurrentUser(updatedProfile, preferences)
|
||||
currentPreferences = preferences
|
||||
m.currentUser.value = updatedUser
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
|
||||
+1
-3
@@ -46,9 +46,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
|
||||
withApi {
|
||||
val newProfile = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName, fullName = fullName, image = image))
|
||||
if (newProfile != null) {
|
||||
chatModel.currentUser.value?.profile?.profileId?.let {
|
||||
chatModel.updateUserProfile(newProfile.toLocalProfile(it))
|
||||
}
|
||||
chatModel.updateCurrentUser(newProfile)
|
||||
profile = newProfile
|
||||
}
|
||||
editProfile.value = false
|
||||
|
||||
@@ -478,6 +478,12 @@
|
||||
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts will be used when available.</string>
|
||||
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts will not be used.</string>
|
||||
<string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts will be required for connection.</string>
|
||||
<string name="network_session_mode_transport_isolation">Transport isolation</string>
|
||||
<string name="network_session_mode_user">Chat profile</string>
|
||||
<string name="network_session_mode_entity">Connection</string>
|
||||
<string name="network_session_mode_user_description">A separate TCP connection (and SOCKS credential) will be used <b>for each chat profile you have in the app</b>.</string>
|
||||
<string name="network_session_mode_entity_description">A separate TCP connection (and SOCKS credential) will be used <b>for each contact and group member</b>.\n<b>Please note</b>: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</string>
|
||||
<string name="update_network_session_mode_question">Update transport isolation mode?</string>
|
||||
<string name="appearance_settings">Appearance</string>
|
||||
<string name="app_version_title">App version</string>
|
||||
<string name="app_version_name">App version: v%s</string>
|
||||
|
||||
@@ -50,7 +50,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
try await apiVerifyToken(token: token, nonce: nonce, code: verification)
|
||||
m.tokenStatus = .active
|
||||
} catch {
|
||||
if let cr = error as? ChatResponse, case .chatCmdError(.errorAgent(.NTF(.AUTH))) = cr {
|
||||
if let cr = error as? ChatResponse, case .chatCmdError(_, .errorAgent(.NTF(.AUTH))) = cr {
|
||||
m.tokenStatus = .expired
|
||||
}
|
||||
logger.error("AppDelegate: didReceiveRemoteNotification: apiVerifyToken or apiIntervalNofication error: \(responseError(error))")
|
||||
|
||||
@@ -16,6 +16,7 @@ final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
@Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get()
|
||||
@Published var currentUser: User?
|
||||
@Published var users: [UserInfo] = []
|
||||
@Published var chatInitialized = false
|
||||
@Published var chatRunning: Bool?
|
||||
@Published var chatDbChanged = false
|
||||
@@ -23,6 +24,8 @@ final class ChatModel: ObservableObject {
|
||||
@Published var chatDbStatus: DBMigrationResult?
|
||||
// list of chat "previews"
|
||||
@Published var chats: [Chat] = []
|
||||
// map of connections network statuses, key is agent connection id
|
||||
@Published var networkStatuses: Dictionary<String, NetworkStatus> = [:]
|
||||
// current chat
|
||||
@Published var chatId: String?
|
||||
@Published var reversedChatItems: [ChatItem] = []
|
||||
@@ -54,6 +57,8 @@ final class ChatModel: ObservableObject {
|
||||
@Published var connReqInv: String?
|
||||
// audio recording and playback
|
||||
@Published var stopPreviousRecPlay: Bool = false // value is not taken into account, only the fact it switches
|
||||
@Published var draft: ComposeState?
|
||||
@Published var draftChatId: String?
|
||||
var callWebView: WKWebView?
|
||||
|
||||
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
|
||||
@@ -127,17 +132,9 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func updateNetworkStatus(_ id: ChatId, _ status: Chat.NetworkStatus) {
|
||||
if let i = getChatIndex(id) {
|
||||
chats[i].serverInfo.networkStatus = status
|
||||
}
|
||||
}
|
||||
|
||||
func replaceChat(_ id: String, _ chat: Chat) {
|
||||
if let i = getChatIndex(id) {
|
||||
let serverInfo = chats[i].serverInfo
|
||||
chats[i] = chat
|
||||
chats[i].serverInfo = serverInfo
|
||||
} else {
|
||||
// invalid state, correcting
|
||||
chats.insert(chat, at: 0)
|
||||
@@ -163,7 +160,7 @@ final class ChatModel: ObservableObject {
|
||||
addChat(Chat(c), at: i)
|
||||
}
|
||||
}
|
||||
NtfManager.shared.setNtfBadgeCount(totalUnreadCount())
|
||||
NtfManager.shared.setNtfBadgeCount(totalUnreadCountForAllUsers())
|
||||
}
|
||||
|
||||
// func addGroup(_ group: SimpleXChat.Group) {
|
||||
@@ -176,7 +173,7 @@ final class ChatModel: ObservableObject {
|
||||
chats[i].chatItems = [cItem]
|
||||
if case .rcvNew = cItem.meta.itemStatus {
|
||||
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1
|
||||
NtfManager.shared.incNtfBadgeCount()
|
||||
increaseUnreadCounter(user: currentUser!)
|
||||
}
|
||||
if i > 0 {
|
||||
if chatId == nil {
|
||||
@@ -257,9 +254,6 @@ final class ChatModel: ObservableObject {
|
||||
// remove from current chat
|
||||
if chatId == cInfo.id {
|
||||
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
||||
if reversedChatItems[i].isRcvNew {
|
||||
NtfManager.shared.decNtfBadgeCount()
|
||||
}
|
||||
_ = withAnimation {
|
||||
self.reversedChatItems.remove(at: i)
|
||||
}
|
||||
@@ -283,6 +277,18 @@ final class ChatModel: ObservableObject {
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateCurrentUser(_ newProfile: Profile, _ preferences: FullPreferences? = nil) {
|
||||
if let current = currentUser {
|
||||
currentUser?.profile = toLocalProfile(current.profile.profileId, newProfile, "")
|
||||
if let preferences = preferences {
|
||||
currentUser?.fullPreferences = preferences
|
||||
}
|
||||
if let current = currentUser, let i = users.firstIndex(where: { $0.user.userId == current.userId }) {
|
||||
users[i].user = current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addLiveDummy(_ chatInfo: ChatInfo) -> ChatItem {
|
||||
let cItem = ChatItem.liveDummy(chatInfo.chatType)
|
||||
withAnimation {
|
||||
@@ -308,7 +314,7 @@ final class ChatModel: ObservableObject {
|
||||
func markChatItemsRead(_ cInfo: ChatInfo) {
|
||||
// update preview
|
||||
_updateChat(cInfo.id) { chat in
|
||||
NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount)
|
||||
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
|
||||
chat.chatStats = ChatStats()
|
||||
}
|
||||
// update current chat
|
||||
@@ -341,8 +347,8 @@ final class ChatModel: ObservableObject {
|
||||
// update preview
|
||||
let markedCount = chat.chatStats.unreadCount - unreadBelow
|
||||
if markedCount > 0 {
|
||||
NtfManager.shared.decNtfBadgeCount(by: markedCount)
|
||||
chat.chatStats.unreadCount -= markedCount
|
||||
self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,7 +366,7 @@ final class ChatModel: ObservableObject {
|
||||
func clearChat(_ cInfo: ChatInfo) {
|
||||
// clear preview
|
||||
if let chat = getChat(cInfo.id) {
|
||||
NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount)
|
||||
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
|
||||
chat.chatItems = []
|
||||
chat.chatStats = ChatStats()
|
||||
chat.chatInfo = cInfo
|
||||
@@ -394,11 +400,29 @@ final class ChatModel: ObservableObject {
|
||||
func decreaseUnreadCounter(_ cInfo: ChatInfo) {
|
||||
if let i = getChatIndex(cInfo.id) {
|
||||
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1
|
||||
decreaseUnreadCounter(user: currentUser!)
|
||||
}
|
||||
}
|
||||
|
||||
func totalUnreadCount() -> Int {
|
||||
chats.reduce(0, { count, chat in count + chat.chatStats.unreadCount })
|
||||
func increaseUnreadCounter(user: User) {
|
||||
changeUnreadCounter(user: user, by: 1)
|
||||
NtfManager.shared.incNtfBadgeCount()
|
||||
}
|
||||
|
||||
func decreaseUnreadCounter(user: User, by: Int = 1) {
|
||||
changeUnreadCounter(user: user, by: -by)
|
||||
NtfManager.shared.decNtfBadgeCount(by: by)
|
||||
}
|
||||
|
||||
private func changeUnreadCounter(user: User, by: Int) {
|
||||
if let i = users.firstIndex(where: { $0.user.id == user.id }) {
|
||||
users[i].unreadCount += by
|
||||
}
|
||||
}
|
||||
|
||||
func totalUnreadCountForAllUsers() -> Int {
|
||||
chats.filter { $0.chatInfo.ntfsEnabled }.reduce(0, { count, chat in count + chat.chatStats.unreadCount }) +
|
||||
users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount })
|
||||
}
|
||||
|
||||
func getPrevChatItem(_ ci: ChatItem) -> ChatItem? {
|
||||
@@ -479,6 +503,14 @@ final class ChatModel: ObservableObject {
|
||||
while i < maxIx && inView(i) { i += 1 }
|
||||
return reversedChatItems[min(i - 1, maxIx)]
|
||||
}
|
||||
|
||||
func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) {
|
||||
networkStatuses[contact.activeConn.agentConnId] = status
|
||||
}
|
||||
|
||||
func contactNetworkStatus(_ contact: Contact) -> NetworkStatus {
|
||||
networkStatuses[contact.activeConn.agentConnId] ?? .unknown
|
||||
}
|
||||
}
|
||||
|
||||
struct UnreadChatItemCounts {
|
||||
@@ -490,62 +522,18 @@ final class Chat: ObservableObject, Identifiable {
|
||||
@Published var chatInfo: ChatInfo
|
||||
@Published var chatItems: [ChatItem]
|
||||
@Published var chatStats: ChatStats
|
||||
@Published var serverInfo = ServerInfo(networkStatus: .unknown)
|
||||
var created = Date.now
|
||||
|
||||
struct ServerInfo: Decodable {
|
||||
var networkStatus: NetworkStatus
|
||||
}
|
||||
|
||||
enum NetworkStatus: Decodable, Equatable {
|
||||
case unknown
|
||||
case connected
|
||||
case disconnected
|
||||
case error(String)
|
||||
|
||||
var statusString: LocalizedStringKey {
|
||||
get {
|
||||
switch self {
|
||||
case .connected: return "connected"
|
||||
case .error: return "error"
|
||||
default: return "connecting"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var statusExplanation: LocalizedStringKey {
|
||||
get {
|
||||
switch self {
|
||||
case .connected: return "You are connected to the server used to receive messages from this contact."
|
||||
case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
|
||||
default: return "Trying to connect to the server used to receive messages from this contact."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var imageName: String {
|
||||
get {
|
||||
switch self {
|
||||
case .unknown: return "circle.dotted"
|
||||
case .connected: return "circle.fill"
|
||||
case .disconnected: return "ellipsis.circle.fill"
|
||||
case .error: return "exclamationmark.circle.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(_ cData: ChatData) {
|
||||
self.chatInfo = cData.chatInfo
|
||||
self.chatItems = cData.chatItems
|
||||
self.chatStats = cData.chatStats
|
||||
}
|
||||
|
||||
init(chatInfo: ChatInfo, chatItems: [ChatItem] = [], chatStats: ChatStats = ChatStats(), serverInfo: ServerInfo = ServerInfo(networkStatus: .unknown)) {
|
||||
init(chatInfo: ChatInfo, chatItems: [ChatItem] = [], chatStats: ChatStats = ChatStats()) {
|
||||
self.chatInfo = chatInfo
|
||||
self.chatItems = chatItems
|
||||
self.chatStats = chatStats
|
||||
self.serverInfo = serverInfo
|
||||
}
|
||||
|
||||
var id: ChatId { get { chatInfo.id } }
|
||||
@@ -554,3 +542,41 @@ final class Chat: ObservableObject, Identifiable {
|
||||
|
||||
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||
}
|
||||
|
||||
enum NetworkStatus: Decodable, Equatable {
|
||||
case unknown
|
||||
case connected
|
||||
case disconnected
|
||||
case error(String)
|
||||
|
||||
var statusString: LocalizedStringKey {
|
||||
get {
|
||||
switch self {
|
||||
case .connected: return "connected"
|
||||
case .error: return "error"
|
||||
default: return "connecting"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var statusExplanation: LocalizedStringKey {
|
||||
get {
|
||||
switch self {
|
||||
case .connected: return "You are connected to the server used to receive messages from this contact."
|
||||
case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
|
||||
default: return "Trying to connect to the server used to receive messages from this contact."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var imageName: String {
|
||||
get {
|
||||
switch self {
|
||||
case .unknown: return "circle.dotted"
|
||||
case .connected: return "circle.fill"
|
||||
case .disconnected: return "ellipsis.circle.fill"
|
||||
case .error: return "exclamationmark.circle.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,8 +165,7 @@ func saveFileFromURL(_ url: URL) -> String? {
|
||||
}
|
||||
|
||||
func generateNewFileName(_ prefix: String, _ ext: String) -> String {
|
||||
let fileName = uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)")
|
||||
return fileName
|
||||
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)")
|
||||
}
|
||||
|
||||
private func uniqueCombine(_ fileName: String) -> String {
|
||||
@@ -191,6 +190,7 @@ private func getTimestamp() -> String {
|
||||
df = DateFormatter()
|
||||
df.dateFormat = "yyyyMMdd_HHmmss"
|
||||
df.locale = Locale(identifier: "US")
|
||||
df.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
tsFormatter = df
|
||||
}
|
||||
return df.string(from: Date())
|
||||
|
||||
@@ -28,7 +28,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
private var granted = false
|
||||
private var prevNtfTime: Dictionary<ChatId, Date> = [:]
|
||||
|
||||
|
||||
// Handle notification when app is in background
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
@@ -38,8 +37,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
let chatModel = ChatModel.shared
|
||||
let action = response.actionIdentifier
|
||||
logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
|
||||
if let userId = content.userInfo["userId"] as? Int64,
|
||||
userId != chatModel.currentUser?.userId {
|
||||
changeActiveUser(userId)
|
||||
}
|
||||
if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact,
|
||||
let chatId = content.userInfo["chatId"] as? String {
|
||||
let chatId = content.userInfo["chatId"] as? String {
|
||||
if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo {
|
||||
Task { await acceptContactRequest(contactRequest) }
|
||||
} else {
|
||||
@@ -189,20 +192,20 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
center.delegate = self
|
||||
}
|
||||
|
||||
func notifyContactRequest(_ contactRequest: UserContactRequest) {
|
||||
func notifyContactRequest(_ user: User, _ contactRequest: UserContactRequest) {
|
||||
logger.debug("NtfManager.notifyContactRequest")
|
||||
addNotification(createContactRequestNtf(contactRequest))
|
||||
addNotification(createContactRequestNtf(user, contactRequest))
|
||||
}
|
||||
|
||||
func notifyContactConnected(_ contact: Contact) {
|
||||
func notifyContactConnected(_ user: User, _ contact: Contact) {
|
||||
logger.debug("NtfManager.notifyContactConnected")
|
||||
addNotification(createContactConnectedNtf(contact))
|
||||
addNotification(createContactConnectedNtf(user, contact))
|
||||
}
|
||||
|
||||
func notifyMessageReceived(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
func notifyMessageReceived(_ user: User, _ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
logger.debug("NtfManager.notifyMessageReceived")
|
||||
if cInfo.ntfsEnabled {
|
||||
addNotification(createMessageReceivedNtf(cInfo, cItem))
|
||||
addNotification(createMessageReceivedNtf(user, cInfo, cItem))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ func apiGetActiveUser() throws -> User? {
|
||||
let r = chatSendCmdSync(.showActiveUser)
|
||||
switch r {
|
||||
case let .activeUser(user): return user
|
||||
case .chatCmdError(.error(.noActiveUser)): return nil
|
||||
case .chatCmdError(_, .error(.noActiveUser)): return nil
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,26 @@ func apiCreateActiveUser(_ p: Profile) throws -> User {
|
||||
throw r
|
||||
}
|
||||
|
||||
func listUsers() throws -> [UserInfo] {
|
||||
let r = chatSendCmdSync(.listUsers)
|
||||
if case let .usersList(users) = r {
|
||||
return users.sorted { $0.user.chatViewName.compare($1.user.chatViewName) == .orderedAscending }
|
||||
}
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetActiveUser(_ userId: Int64) throws -> User {
|
||||
let r = chatSendCmdSync(.apiSetActiveUser(userId: userId))
|
||||
if case let .activeUser(user) = r { return user }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool) throws {
|
||||
let r = chatSendCmdSync(.apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiStartChat() throws -> Bool {
|
||||
let r = chatSendCmdSync(.startChat(subscribe: true, expire: true))
|
||||
switch r {
|
||||
@@ -189,20 +209,21 @@ func apiStorageEncryption(currentKey: String = "", newKey: String = "") async th
|
||||
}
|
||||
|
||||
func apiGetChats() throws -> [ChatData] {
|
||||
let r = chatSendCmdSync(.apiGetChats)
|
||||
if case let .apiChats(chats) = r { return chats }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiGetChats: no current user") }
|
||||
let r = chatSendCmdSync(.apiGetChats(userId: userId))
|
||||
if case let .apiChats(_, chats) = r { return chats }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetChat(type: ChatType, id: Int64, search: String = "") throws -> Chat {
|
||||
let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: 50), search: search))
|
||||
if case let .apiChat(chat) = r { return Chat.init(chat) }
|
||||
if case let .apiChat(_, chat) = r { return Chat.init(chat) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> [ChatItem] {
|
||||
let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination, search: search))
|
||||
if case let .apiChat(chat) = r { return chat.chatItems }
|
||||
if case let .apiChat(_, chat) = r { return chat.chatItems }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -227,7 +248,7 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6
|
||||
var cItem: ChatItem!
|
||||
let endTask = beginBGTask({ if cItem != nil { chatModel.messageDelivery.removeValue(forKey: cItem.id) } })
|
||||
r = await chatSendCmd(cmd, bgTask: false)
|
||||
if case let .newChatItem(aChatItem) = r {
|
||||
if case let .newChatItem(_, aChatItem) = r {
|
||||
cItem = aChatItem.chatItem
|
||||
chatModel.messageDelivery[cItem.id] = endTask
|
||||
return cItem
|
||||
@@ -239,7 +260,7 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6
|
||||
return nil
|
||||
} else {
|
||||
r = await chatSendCmd(cmd, bgDelay: msgDelay)
|
||||
if case let .newChatItem(aChatItem) = r {
|
||||
if case let .newChatItem(_, aChatItem) = r {
|
||||
return aChatItem.chatItem
|
||||
}
|
||||
sendMessageErrorAlert(r)
|
||||
@@ -257,13 +278,13 @@ private func sendMessageErrorAlert(_ r: ChatResponse) {
|
||||
|
||||
func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool = false) async throws -> ChatItem {
|
||||
let r = await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, msg: msg, live: live), bgDelay: msgDelay)
|
||||
if case let .chatItemUpdated(aChatItem) = r { return aChatItem.chatItem }
|
||||
if case let .chatItemUpdated(_, aChatItem) = r { return aChatItem.chatItem }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) async throws -> (ChatItem, ChatItem?) {
|
||||
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay)
|
||||
if case let .chatItemDeleted(deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) }
|
||||
if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -271,7 +292,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
|
||||
let r = chatSendCmdSync(.apiGetNtfToken)
|
||||
switch r {
|
||||
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
|
||||
case .chatCmdError(.errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
|
||||
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
|
||||
default:
|
||||
logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)")
|
||||
return (nil, nil, .off)
|
||||
@@ -310,18 +331,21 @@ func apiDeleteToken(token: DeviceToken) async throws {
|
||||
}
|
||||
|
||||
func getUserSMPServers() throws -> ([ServerCfg], [String]) {
|
||||
let r = chatSendCmdSync(.getUserSMPServers)
|
||||
if case let .userSMPServers(smpServers, presetServers) = r { return (smpServers, presetServers) }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("getUserSMPServers: no current user") }
|
||||
let r = chatSendCmdSync(.apiGetUserSMPServers(userId: userId))
|
||||
if case let .userSMPServers(_, smpServers, presetServers) = r { return (smpServers, presetServers) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func setUserSMPServers(smpServers: [ServerCfg]) async throws {
|
||||
try await sendCommandOkResp(.setUserSMPServers(smpServers: smpServers))
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("setUserSMPServers: no current user") }
|
||||
try await sendCommandOkResp(.apiSetUserSMPServers(userId: userId, smpServers: smpServers))
|
||||
}
|
||||
|
||||
func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure> {
|
||||
let r = await chatSendCmd(.testSMPServer(smpServer: smpServer))
|
||||
if case let .smpTestResult(testFailure) = r {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("testSMPServer: no current user") }
|
||||
let r = await chatSendCmd(.testSMPServer(userId: userId, smpServer: smpServer))
|
||||
if case let .smpTestResult(_, testFailure) = r {
|
||||
if let t = testFailure {
|
||||
return .failure(t)
|
||||
}
|
||||
@@ -331,13 +355,15 @@ func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure>
|
||||
}
|
||||
|
||||
func getChatItemTTL() throws -> ChatItemTTL {
|
||||
let r = chatSendCmdSync(.apiGetChatItemTTL)
|
||||
if case let .chatItemTTL(chatItemTTL) = r { return ChatItemTTL(chatItemTTL) }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("getChatItemTTL: no current user") }
|
||||
let r = chatSendCmdSync(.apiGetChatItemTTL(userId: userId))
|
||||
if case let .chatItemTTL(_, chatItemTTL) = r { return ChatItemTTL(chatItemTTL) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func setChatItemTTL(_ chatItemTTL: ChatItemTTL) async throws {
|
||||
try await sendCommandOkResp(.apiSetChatItemTTL(seconds: chatItemTTL.seconds))
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("setChatItemTTL: no current user") }
|
||||
try await sendCommandOkResp(.apiSetChatItemTTL(userId: userId, seconds: chatItemTTL.seconds))
|
||||
}
|
||||
|
||||
func getNetworkConfig() async throws -> NetCfg? {
|
||||
@@ -358,13 +384,13 @@ func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) a
|
||||
|
||||
func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profile?) {
|
||||
let r = await chatSendCmd(.apiContactInfo(contactId: contactId))
|
||||
if case let .contactInfo(_, connStats, customUserProfile) = r { return (connStats, customUserProfile) }
|
||||
if case let .contactInfo(_, _, connStats, customUserProfile) = r { return (connStats, customUserProfile) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (ConnectionStats?) {
|
||||
let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .groupMemberInfo(_, _, connStats_) = r { return (connStats_) }
|
||||
if case let .groupMemberInfo(_, _, _, connStats_) = r { return (connStats_) }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -378,44 +404,52 @@ func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) async throws
|
||||
|
||||
func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) {
|
||||
let r = await chatSendCmd(.apiGetContactCode(contactId: contactId))
|
||||
if case let .contactCode(contact, connectionCode) = r { return (contact, connectionCode) }
|
||||
if case let .contactCode(_, contact, connectionCode) = r { return (contact, connectionCode) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, String) {
|
||||
let r = chatSendCmdSync(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .groupMemberCode(_, member, connectionCode) = r { return (member, connectionCode) }
|
||||
if case let .groupMemberCode(_, _, member, connectionCode) = r { return (member, connectionCode) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiVerifyContact(_ contactId: Int64, connectionCode: String?) -> (Bool, String)? {
|
||||
let r = chatSendCmdSync(.apiVerifyContact(contactId: contactId, connectionCode: connectionCode))
|
||||
if case let .connectionVerified(verified, expectedCode) = r { return (verified, expectedCode) }
|
||||
if case let .connectionVerified(_, verified, expectedCode) = r { return (verified, expectedCode) }
|
||||
logger.error("apiVerifyContact error: \(String(describing: r))")
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCode: String?) -> (Bool, String)? {
|
||||
let r = chatSendCmdSync(.apiVerifyGroupMember(groupId: groupId, groupMemberId: groupMemberId, connectionCode: connectionCode))
|
||||
if case let .connectionVerified(verified, expectedCode) = r { return (verified, expectedCode) }
|
||||
if case let .connectionVerified(_, verified, expectedCode) = r { return (verified, expectedCode) }
|
||||
logger.error("apiVerifyGroupMember error: \(String(describing: r))")
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiAddContact() async -> String? {
|
||||
let r = await chatSendCmd(.addContact, bgTask: false)
|
||||
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else {
|
||||
logger.error("apiAddContact: no current user")
|
||||
return nil
|
||||
}
|
||||
let r = await chatSendCmd(.apiAddContact(userId: userId), bgTask: false)
|
||||
if case let .invitation(_, connReqInvitation) = r { return connReqInvitation }
|
||||
connectionErrorAlert(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiConnect(connReq: String) async -> ConnReqType? {
|
||||
let r = await chatSendCmd(.connect(connReq: connReq))
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else {
|
||||
logger.error("apiConnect: no current user")
|
||||
return nil
|
||||
}
|
||||
let r = await chatSendCmd(.apiConnect(userId: userId, connReq: connReq))
|
||||
let am = AlertManager.shared
|
||||
switch r {
|
||||
case .sentConfirmation: return .invitation
|
||||
case .sentInvitation: return .contact
|
||||
case let .contactAlreadyExists(contact):
|
||||
case let .contactAlreadyExists(_, contact):
|
||||
let m = ChatModel.shared
|
||||
if let c = m.getContactChat(contact.contactId) {
|
||||
await MainActor.run { m.chatId = c.id }
|
||||
@@ -425,19 +459,19 @@ func apiConnect(connReq: String) async -> ConnReqType? {
|
||||
message: "You are already connected to \(contact.displayName)."
|
||||
)
|
||||
return nil
|
||||
case .chatCmdError(.error(.invalidConnReq)):
|
||||
case .chatCmdError(_, .error(.invalidConnReq)):
|
||||
am.showAlertMsg(
|
||||
title: "Invalid connection link",
|
||||
message: "Please check that you used the correct link or ask your contact to send you another one."
|
||||
)
|
||||
return nil
|
||||
case .chatCmdError(.errorAgent(.SMP(.AUTH))):
|
||||
case .chatCmdError(_, .errorAgent(.SMP(.AUTH))):
|
||||
am.showAlertMsg(
|
||||
title: "Connection error (AUTH)",
|
||||
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
|
||||
)
|
||||
return nil
|
||||
case let .chatCmdError(.errorAgent(.INTERNAL(internalErr))):
|
||||
case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))):
|
||||
if internalErr == "SEUniqueID" {
|
||||
am.showAlertMsg(
|
||||
title: "Already connected?",
|
||||
@@ -484,7 +518,7 @@ func deleteChat(_ chat: Chat) async {
|
||||
|
||||
func apiClearChat(type: ChatType, id: Int64) async throws -> ChatInfo {
|
||||
let r = await chatSendCmd(.apiClearChat(type: type, id: id), bgTask: false)
|
||||
if case let .chatCleared(updatedChatInfo) = r { return updatedChatInfo }
|
||||
if case let .chatCleared(_, updatedChatInfo) = r { return updatedChatInfo }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -499,64 +533,70 @@ func clearChat(_ chat: Chat) async {
|
||||
}
|
||||
|
||||
func apiListContacts() throws -> [Contact] {
|
||||
let r = chatSendCmdSync(.listContacts)
|
||||
if case let .contactsList(contacts) = r { return contacts }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiListContacts: no current user") }
|
||||
let r = chatSendCmdSync(.apiListContacts(userId: userId))
|
||||
if case let .contactsList(_, contacts) = r { return contacts }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiUpdateProfile(profile: Profile) async throws -> Profile? {
|
||||
let r = await chatSendCmd(.apiUpdateProfile(profile: profile))
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiUpdateProfile: no current user") }
|
||||
let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile))
|
||||
switch r {
|
||||
case .userProfileNoChange: return nil
|
||||
case let .userProfileUpdated(_, toProfile): return toProfile
|
||||
case let .userProfileUpdated(_, _, toProfile): return toProfile
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func apiSetContactPrefs(contactId: Int64, preferences: Preferences) async throws -> Contact? {
|
||||
let r = await chatSendCmd(.apiSetContactPrefs(contactId: contactId, preferences: preferences))
|
||||
if case let .contactPrefsUpdated(_, toContact) = r { return toContact }
|
||||
if case let .contactPrefsUpdated(_, _, toContact) = r { return toContact }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetContactAlias(contactId: Int64, localAlias: String) async throws -> Contact? {
|
||||
let r = await chatSendCmd(.apiSetContactAlias(contactId: contactId, localAlias: localAlias))
|
||||
if case let .contactAliasUpdated(toContact) = r { return toContact }
|
||||
if case let .contactAliasUpdated(_, toContact) = r { return toContact }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetConnectionAlias(connId: Int64, localAlias: String) async throws -> PendingContactConnection? {
|
||||
let r = await chatSendCmd(.apiSetConnectionAlias(connId: connId, localAlias: localAlias))
|
||||
if case let .connectionAliasUpdated(toConnection) = r { return toConnection }
|
||||
if case let .connectionAliasUpdated(_, toConnection) = r { return toConnection }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiCreateUserAddress() async throws -> String {
|
||||
let r = await chatSendCmd(.createMyAddress)
|
||||
if case let .userContactLinkCreated(connReq) = r { return connReq }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiCreateUserAddress: no current user") }
|
||||
let r = await chatSendCmd(.apiCreateMyAddress(userId: userId))
|
||||
if case let .userContactLinkCreated(_, connReq) = r { return connReq }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteUserAddress() async throws {
|
||||
let r = await chatSendCmd(.deleteMyAddress)
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiDeleteUserAddress: no current user") }
|
||||
let r = await chatSendCmd(.apiDeleteMyAddress(userId: userId))
|
||||
if case .userContactLinkDeleted = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetUserAddress() throws -> UserContactLink? {
|
||||
let r = chatSendCmdSync(.showMyAddress)
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiGetUserAddress: no current user") }
|
||||
let r = chatSendCmdSync(.apiShowMyAddress(userId: userId))
|
||||
switch r {
|
||||
case let .userContactLink(contactLink): return contactLink
|
||||
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
|
||||
case let .userContactLink(_, contactLink): return contactLink
|
||||
case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContactLink? {
|
||||
let r = await chatSendCmd(.addressAutoAccept(autoAccept: autoAccept))
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("userAddressAutoAccept: no current user") }
|
||||
let r = await chatSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept))
|
||||
switch r {
|
||||
case let .userContactLinkUpdated(contactLink): return contactLink
|
||||
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
|
||||
case let .userContactLinkUpdated(_, contactLink): return contactLink
|
||||
case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
@@ -565,8 +605,8 @@ func apiAcceptContactRequest(contactReqId: Int64) async -> Contact? {
|
||||
let r = await chatSendCmd(.apiAcceptContact(contactReqId: contactReqId))
|
||||
let am = AlertManager.shared
|
||||
|
||||
if case let .acceptingContactRequest(contact) = r { return contact }
|
||||
if case .chatCmdError(.errorAgent(.SMP(.AUTH))) = r {
|
||||
if case let .acceptingContactRequest(_, contact) = r { return contact }
|
||||
if case .chatCmdError(_, .errorAgent(.SMP(.AUTH))) = r {
|
||||
am.showAlertMsg(
|
||||
title: "Connection error (AUTH)",
|
||||
message: "Sender may have deleted the connection request."
|
||||
@@ -605,7 +645,7 @@ func receiveFile(fileId: Int64) async {
|
||||
func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.receiveFile(fileId: fileId, inline: inline))
|
||||
let am = AlertManager.shared
|
||||
if case let .rcvFileAccepted(chatItem) = r { return chatItem }
|
||||
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
||||
if case .rcvFileAcceptedSndCancelled = r {
|
||||
am.showAlertMsg(
|
||||
title: "Cannot receive file",
|
||||
@@ -614,7 +654,7 @@ func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
|
||||
} else if !networkErrorAlert(r) {
|
||||
logger.error("apiReceiveFile error: \(String(describing: r))")
|
||||
switch r {
|
||||
case .chatCmdError(.error(.fileAlreadyReceiving)):
|
||||
case .chatCmdError(_, .error(.fileAlreadyReceiving)):
|
||||
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
|
||||
default:
|
||||
am.showAlertMsg(
|
||||
@@ -629,13 +669,13 @@ func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
|
||||
func networkErrorAlert(_ r: ChatResponse) -> Bool {
|
||||
let am = AlertManager.shared
|
||||
switch r {
|
||||
case let .chatCmdError(.errorAgent(.BROKER(addr, .TIMEOUT))):
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
|
||||
am.showAlertMsg(
|
||||
title: "Connection timeout",
|
||||
message: "Please check your network connection with \(serverHostname(addr)) and try again."
|
||||
)
|
||||
return true
|
||||
case let .chatCmdError(.errorAgent(.BROKER(addr, .NETWORK))):
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))):
|
||||
am.showAlertMsg(
|
||||
title: "Connection error",
|
||||
message: "Please check your network connection with \(serverHostname(addr)) and try again."
|
||||
@@ -748,14 +788,15 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
|
||||
}
|
||||
|
||||
func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo {
|
||||
let r = chatSendCmdSync(.newGroup(groupProfile: p))
|
||||
if case let .groupCreated(groupInfo) = r { return groupInfo }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiNewGroup: no current user") }
|
||||
let r = chatSendCmdSync(.apiNewGroup(userId: userId, groupProfile: p))
|
||||
if case let .groupCreated(_, groupInfo) = r { return groupInfo }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember {
|
||||
let r = await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole))
|
||||
if case let .sentGroupInvitation(_, _, member) = r { return member }
|
||||
if case let .sentGroupInvitation(_, _, _, member) = r { return member }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -768,22 +809,22 @@ enum JoinGroupResult {
|
||||
func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult {
|
||||
let r = await chatSendCmd(.apiJoinGroup(groupId: groupId))
|
||||
switch r {
|
||||
case let .userAcceptedGroupSent(groupInfo, _): return .joined(groupInfo: groupInfo)
|
||||
case .chatCmdError(.errorAgent(.SMP(.AUTH))): return .invitationRemoved
|
||||
case .chatCmdError(.errorStore(.groupNotFound)): return .groupNotFound
|
||||
case let .userAcceptedGroupSent(_, groupInfo, _): return .joined(groupInfo: groupInfo)
|
||||
case .chatCmdError(_, .errorAgent(.SMP(.AUTH))): return .invitationRemoved
|
||||
case .chatCmdError(_, .errorStore(.groupNotFound)): return .groupNotFound
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func apiRemoveMember(_ groupId: Int64, _ memberId: Int64) async throws -> GroupMember {
|
||||
let r = await chatSendCmd(.apiRemoveMember(groupId: groupId, memberId: memberId), bgTask: false)
|
||||
if case let .userDeletedMember(_, member) = r { return member }
|
||||
if case let .userDeletedMember(_, _, member) = r { return member }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiMemberRole(_ groupId: Int64, _ memberId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember {
|
||||
let r = await chatSendCmd(.apiMemberRole(groupId: groupId, memberId: memberId, memberRole: memberRole), bgTask: false)
|
||||
if case let .memberRoleUser(_, member, _, _) = r { return member }
|
||||
if case let .memberRoleUser(_, _, member, _, _) = r { return member }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -798,19 +839,19 @@ func leaveGroup(_ groupId: Int64) async {
|
||||
|
||||
func apiLeaveGroup(_ groupId: Int64) async throws -> GroupInfo {
|
||||
let r = await chatSendCmd(.apiLeaveGroup(groupId: groupId), bgTask: false)
|
||||
if case let .leftMemberUser(groupInfo) = r { return groupInfo }
|
||||
if case let .leftMemberUser(_, groupInfo) = r { return groupInfo }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiListMembers(_ groupId: Int64) async -> [GroupMember] {
|
||||
let r = await chatSendCmd(.apiListMembers(groupId: groupId))
|
||||
if case let .groupMembers(group) = r { return group.members }
|
||||
if case let .groupMembers(_, group) = r { return group.members }
|
||||
return []
|
||||
}
|
||||
|
||||
func apiListMembersSync(_ groupId: Int64) -> [GroupMember] {
|
||||
let r = chatSendCmdSync(.apiListMembers(groupId: groupId))
|
||||
if case let .groupMembers(group) = r { return group.members }
|
||||
if case let .groupMembers(_, group) = r { return group.members }
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -824,13 +865,13 @@ func filterMembersToAdd(_ ms: [GroupMember]) -> [Contact] {
|
||||
|
||||
func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws -> GroupInfo {
|
||||
let r = await chatSendCmd(.apiUpdateGroupProfile(groupId: groupId, groupProfile: groupProfile))
|
||||
if case let .groupUpdated(toGroup) = r { return toGroup }
|
||||
if case let .groupUpdated(_, toGroup) = r { return toGroup }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiCreateGroupLink(_ groupId: Int64) async throws -> String {
|
||||
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId))
|
||||
if case let .groupLinkCreated(_, connReq) = r { return connReq }
|
||||
if case let .groupLinkCreated(_, _, connReq) = r { return connReq }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -843,9 +884,9 @@ func apiDeleteGroupLink(_ groupId: Int64) async throws {
|
||||
func apiGetGroupLink(_ groupId: Int64) throws -> String? {
|
||||
let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId))
|
||||
switch r {
|
||||
case let .groupLink(_, connReq):
|
||||
case let .groupLink(_, _, connReq):
|
||||
return connReq
|
||||
case .chatCmdError(chatError: .errorStore(storeError: .groupLinkNotFound)):
|
||||
case .chatCmdError(_, chatError: .errorStore(storeError: .groupLinkNotFound)):
|
||||
return nil
|
||||
default: throw r
|
||||
}
|
||||
@@ -884,20 +925,17 @@ func startChat() throws {
|
||||
let m = ChatModel.shared
|
||||
try setNetworkConfig(getNetCfg())
|
||||
let justStarted = try apiStartChat()
|
||||
m.users = try listUsers()
|
||||
if justStarted {
|
||||
m.userAddress = try apiGetUserAddress()
|
||||
(m.userSMPServers, m.presetSMPServers) = try getUserSMPServers()
|
||||
m.chatItemTTL = try getChatItemTTL()
|
||||
let chats = try apiGetChats()
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCount())
|
||||
try getUserChatData()
|
||||
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers())
|
||||
try refreshCallInvitations()
|
||||
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
|
||||
if let token = m.deviceToken {
|
||||
registerToken(token: token)
|
||||
}
|
||||
withAnimation {
|
||||
m.onboardingStage = m.onboardingStage == .step2_CreateProfile
|
||||
m.onboardingStage = m.onboardingStage == .step2_CreateProfile && m.users.count == 1
|
||||
? .step3_SetNotificationsMode
|
||||
: .onboardingComplete
|
||||
}
|
||||
@@ -907,6 +945,30 @@ func startChat() throws {
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
}
|
||||
|
||||
func changeActiveUser(_ userId: Int64) {
|
||||
do {
|
||||
try changeActiveUser_(userId)
|
||||
} catch let error {
|
||||
logger.error("Unable to set active user: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
func changeActiveUser_(_ userId: Int64) throws {
|
||||
let m = ChatModel.shared
|
||||
m.currentUser = try apiSetActiveUser(userId)
|
||||
m.users = try listUsers()
|
||||
try getUserChatData()
|
||||
}
|
||||
|
||||
func getUserChatData() throws {
|
||||
let m = ChatModel.shared
|
||||
m.userAddress = try apiGetUserAddress()
|
||||
(m.userSMPServers, m.presetSMPServers) = try getUserSMPServers()
|
||||
m.chatItemTTL = try getChatItemTTL()
|
||||
let chats = try apiGetChats()
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
}
|
||||
|
||||
class ChatReceiver {
|
||||
private var receiveLoop: Task<Void, Never>?
|
||||
private var receiveMessages = true
|
||||
@@ -950,25 +1012,31 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
m.terminalItems.append(.resp(.now, res))
|
||||
logger.debug("processReceivedMsg: \(res.responseType)")
|
||||
switch res {
|
||||
case let .newContactConnection(connection):
|
||||
m.updateContactConnection(connection)
|
||||
case let .contactConnectionDeleted(connection):
|
||||
m.removeChat(connection.id)
|
||||
case let .contactConnected(contact, _):
|
||||
if contact.directOrUsed {
|
||||
case let .newContactConnection(user, connection):
|
||||
if active(user) {
|
||||
m.updateContactConnection(connection)
|
||||
}
|
||||
case let .contactConnectionDeleted(user, connection):
|
||||
if active(user) {
|
||||
m.removeChat(connection.id)
|
||||
}
|
||||
case let .contactConnected(user, contact, _):
|
||||
if active(user) && contact.directOrUsed {
|
||||
m.updateContact(contact)
|
||||
m.dismissConnReqView(contact.activeConn.id)
|
||||
m.removeChat(contact.activeConn.id)
|
||||
m.updateNetworkStatus(contact.id, .connected)
|
||||
NtfManager.shared.notifyContactConnected(contact)
|
||||
NtfManager.shared.notifyContactConnected(user, contact)
|
||||
}
|
||||
case let .contactConnecting(contact):
|
||||
if contact.directOrUsed {
|
||||
m.setContactNetworkStatus(contact, .connected)
|
||||
case let .contactConnecting(user, contact):
|
||||
if active(user) && contact.directOrUsed {
|
||||
m.updateContact(contact)
|
||||
m.dismissConnReqView(contact.activeConn.id)
|
||||
m.removeChat(contact.activeConn.id)
|
||||
}
|
||||
case let .receivedContactRequest(contactRequest):
|
||||
case let .receivedContactRequest(user, contactRequest):
|
||||
if !active(user) { return }
|
||||
|
||||
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
|
||||
if m.hasChat(contactRequest.id) {
|
||||
m.updateChatInfo(cInfo)
|
||||
@@ -977,15 +1045,15 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
chatInfo: cInfo,
|
||||
chatItems: []
|
||||
))
|
||||
NtfManager.shared.notifyContactRequest(contactRequest)
|
||||
NtfManager.shared.notifyContactRequest(user, contactRequest)
|
||||
}
|
||||
case let .contactUpdated(toContact):
|
||||
let cInfo = ChatInfo.direct(contact: toContact)
|
||||
if m.hasChat(toContact.id) {
|
||||
case let .contactUpdated(user, toContact):
|
||||
if active(user) && m.hasChat(toContact.id) {
|
||||
let cInfo = ChatInfo.direct(contact: toContact)
|
||||
m.updateChatInfo(cInfo)
|
||||
}
|
||||
case let .contactsMerged(intoContact, mergedContact):
|
||||
if m.hasChat(mergedContact.id) {
|
||||
case let .contactsMerged(user, intoContact, mergedContact):
|
||||
if active(user) && m.hasChat(mergedContact.id) {
|
||||
if m.chatId == mergedContact.id {
|
||||
m.chatId = intoContact.id
|
||||
}
|
||||
@@ -995,18 +1063,30 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
updateContactsStatus(contactRefs, status: .connected)
|
||||
case let .contactsDisconnected(_, contactRefs):
|
||||
updateContactsStatus(contactRefs, status: .disconnected)
|
||||
case let .contactSubError(contact, chatError):
|
||||
case let .contactSubError(user, contact, chatError):
|
||||
if active(user) {
|
||||
m.updateContact(contact)
|
||||
}
|
||||
processContactSubError(contact, chatError)
|
||||
case let .contactSubSummary(contactSubscriptions):
|
||||
case let .contactSubSummary(user, contactSubscriptions):
|
||||
for sub in contactSubscriptions {
|
||||
if active(user) {
|
||||
m.updateContact(sub.contact)
|
||||
}
|
||||
if let err = sub.contactError {
|
||||
processContactSubError(sub.contact, err)
|
||||
} else {
|
||||
m.updateContact(sub.contact)
|
||||
m.updateNetworkStatus(sub.contact.id, .connected)
|
||||
m.setContactNetworkStatus(sub.contact, .connected)
|
||||
}
|
||||
}
|
||||
case let .newChatItem(aChatItem):
|
||||
case let .newChatItem(user, aChatItem):
|
||||
if !active(user) {
|
||||
if case .rcvNew = aChatItem.chatItem.meta.itemStatus, aChatItem.chatInfo.ntfsEnabled {
|
||||
m.increaseUnreadCounter(user: user)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
m.addChatItem(cInfo, cItem)
|
||||
@@ -1022,9 +1102,11 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
}
|
||||
if cItem.showNotification {
|
||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
}
|
||||
case let .chatItemStatusUpdated(aChatItem):
|
||||
case let .chatItemStatusUpdated(user, aChatItem):
|
||||
if !active(user) { return }
|
||||
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
var res = false
|
||||
@@ -1032,7 +1114,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
res = m.upsertChatItem(cInfo, cItem)
|
||||
}
|
||||
if res {
|
||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
} else if let endTask = m.messageDelivery[cItem.id] {
|
||||
switch cItem.meta.itemStatus {
|
||||
case .sndSent: endTask()
|
||||
@@ -1041,48 +1123,87 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
default: break
|
||||
}
|
||||
}
|
||||
case let .chatItemUpdated(aChatItem):
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
case let .chatItemDeleted(deletedChatItem, toChatItem, _):
|
||||
case let .chatItemUpdated(user, aChatItem):
|
||||
if active(user) {
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
}
|
||||
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
|
||||
if !active(user) {
|
||||
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled {
|
||||
m.decreaseUnreadCounter(user: user)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let toChatItem = toChatItem {
|
||||
_ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem)
|
||||
} else {
|
||||
m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem)
|
||||
}
|
||||
case let .receivedGroupInvitation(groupInfo, _, _):
|
||||
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
|
||||
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
|
||||
case let .userAcceptedGroupSent(groupInfo, hostContact):
|
||||
case let .receivedGroupInvitation(user, groupInfo, _, _):
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
|
||||
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
|
||||
}
|
||||
case let .userAcceptedGroupSent(user, groupInfo, hostContact):
|
||||
if !active(user) { return }
|
||||
|
||||
m.updateGroup(groupInfo)
|
||||
if let hostContact = hostContact {
|
||||
m.dismissConnReqView(hostContact.activeConn.id)
|
||||
m.removeChat(hostContact.activeConn.id)
|
||||
}
|
||||
case let .joinedGroupMemberConnecting(groupInfo, _, member):
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
case let .deletedMemberUser(groupInfo, _): // TODO update user member
|
||||
m.updateGroup(groupInfo)
|
||||
case let .deletedMember(groupInfo, _, deletedMember):
|
||||
_ = m.upsertGroupMember(groupInfo, deletedMember)
|
||||
case let .leftMember(groupInfo, member):
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
case let .groupDeleted(groupInfo, _): // TODO update user member
|
||||
m.updateGroup(groupInfo)
|
||||
case let .userJoinedGroup(groupInfo):
|
||||
m.updateGroup(groupInfo)
|
||||
case let .joinedGroupMember(groupInfo, member):
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
case let .connectedToGroupMember(groupInfo, member):
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
case let .groupUpdated(toGroup):
|
||||
m.updateGroup(toGroup)
|
||||
case let .rcvFileStart(aChatItem):
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
case let .rcvFileComplete(aChatItem):
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
case let .sndFileStart(aChatItem, _):
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
case let .sndFileComplete(aChatItem, _):
|
||||
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
case let .deletedMemberUser(user, groupInfo, _): // TODO update user member
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
case let .deletedMember(user, groupInfo, _, deletedMember):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, deletedMember)
|
||||
}
|
||||
case let .leftMember(user, groupInfo, member):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
case let .groupDeleted(user, groupInfo, _): // TODO update user member
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
case let .userJoinedGroup(user, groupInfo):
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
case let .joinedGroupMember(user, groupInfo, member):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
case let .connectedToGroupMember(user, groupInfo, member):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
case let .groupUpdated(user, toGroup):
|
||||
if active(user) {
|
||||
m.updateGroup(toGroup)
|
||||
}
|
||||
case let .rcvFileStart(user, aChatItem):
|
||||
if active(user) {
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
}
|
||||
case let .rcvFileComplete(user, aChatItem):
|
||||
if active(user) {
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
}
|
||||
case let .sndFileStart(user, aChatItem, _):
|
||||
if active(user) {
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
}
|
||||
case let .sndFileComplete(user, aChatItem, _):
|
||||
if !active(user) { return }
|
||||
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
let cItem = aChatItem.chatItem
|
||||
let mc = cItem.content.msgContent
|
||||
@@ -1107,7 +1228,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
// logger.debug("reportNewIncomingVoIPPushPayload success for \(contact.id)")
|
||||
// }
|
||||
// }
|
||||
case let .callOffer(contact, callType, offer, sharedKey, _):
|
||||
case let .callOffer(_, contact, callType, offer, sharedKey, _):
|
||||
withCall(contact) { call in
|
||||
call.callState = .offerReceived
|
||||
call.peerMedia = callType.media
|
||||
@@ -1125,16 +1246,16 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
relay: useRelay
|
||||
)
|
||||
}
|
||||
case let .callAnswer(contact, answer):
|
||||
case let .callAnswer(_, contact, answer):
|
||||
withCall(contact) { call in
|
||||
call.callState = .answerReceived
|
||||
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
|
||||
}
|
||||
case let .callExtraInfo(contact, extraInfo):
|
||||
case let .callExtraInfo(_, contact, extraInfo):
|
||||
withCall(contact) { _ in
|
||||
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
|
||||
}
|
||||
case let .callEnded(contact):
|
||||
case let .callEnded(_, contact):
|
||||
if let invitation = m.callInvitations.removeValue(forKey: contact.id) {
|
||||
CallController.shared.reportCallRemoteEnded(invitation: invitation)
|
||||
}
|
||||
@@ -1148,6 +1269,10 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
}
|
||||
|
||||
func active(_ user: User) -> Bool {
|
||||
user.id == m.currentUser?.id
|
||||
}
|
||||
|
||||
func withCall(_ contact: Contact, _ perform: (Call) -> Void) {
|
||||
if let call = m.activeCall, call.contact.apiId == contact.apiId {
|
||||
perform(call)
|
||||
@@ -1163,27 +1288,26 @@ func chatItemSimpleUpdate(_ aChatItem: AChatItem) {
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
if m.upsertChatItem(cInfo, cItem) {
|
||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
NtfManager.shared.notifyMessageReceived(m.currentUser!, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
|
||||
func updateContactsStatus(_ contactRefs: [ContactRef], status: Chat.NetworkStatus) {
|
||||
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) {
|
||||
let m = ChatModel.shared
|
||||
for c in contactRefs {
|
||||
m.updateNetworkStatus(c.id, status)
|
||||
m.networkStatuses[c.agentConnId] = status
|
||||
}
|
||||
}
|
||||
|
||||
func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
|
||||
let m = ChatModel.shared
|
||||
m.updateContact(contact)
|
||||
var err: String
|
||||
switch chatError {
|
||||
case .errorAgent(agentError: .BROKER(_, .NETWORK)): err = "network"
|
||||
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
|
||||
default: err = String(describing: chatError)
|
||||
}
|
||||
m.updateNetworkStatus(contact.id, .error(err))
|
||||
m.setContactNetworkStatus(contact, .error(err))
|
||||
}
|
||||
|
||||
func refreshCallInvitations() throws {
|
||||
@@ -1215,3 +1339,15 @@ private struct UserResponse: Decodable {
|
||||
var user: User?
|
||||
var error: String?
|
||||
}
|
||||
|
||||
struct RuntimeError: Error {
|
||||
let message: String
|
||||
|
||||
init(_ message: String) {
|
||||
self.message = message
|
||||
}
|
||||
|
||||
public var localizedDescription: String {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ struct SimpleXApp: App {
|
||||
enteredBackground = ProcessInfo.processInfo.systemUptime
|
||||
}
|
||||
doAuthenticate = false
|
||||
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCount())
|
||||
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
|
||||
case .active:
|
||||
if chatModel.chatRunning == true {
|
||||
ChatReceiver.shared.start()
|
||||
|
||||
@@ -29,6 +29,10 @@ struct IncomingCallView: View {
|
||||
private func incomingCall(_ invitation: RcvCallInvitation) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
if m.users.count > 1 {
|
||||
ProfileImage(imageStr: invitation.user.image, color: .white)
|
||||
.frame(width: 24, height: 24)
|
||||
}
|
||||
Image(systemName: invitation.callType.media == .video ? "video.fill" : "phone.fill").foregroundColor(.green)
|
||||
Text(invitation.callTypeText)
|
||||
}
|
||||
@@ -82,6 +86,8 @@ struct IncomingCallView: View {
|
||||
struct IncomingCallView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CallController.shared.activeCallInvitation = RcvCallInvitation.sampleData
|
||||
return IncomingCallView()
|
||||
let m = ChatModel()
|
||||
m.users = [UserInfo.sampleData, UserInfo.sampleData]
|
||||
return IncomingCallView().environmentObject(m)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,14 +263,14 @@ struct ChatInfoView: View {
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.system(size: 14))
|
||||
Spacer()
|
||||
Text(chat.serverInfo.networkStatus.statusString)
|
||||
Text(chatModel.contactNetworkStatus(contact).statusString)
|
||||
.foregroundColor(.secondary)
|
||||
serverImage()
|
||||
}
|
||||
}
|
||||
|
||||
private func serverImage() -> some View {
|
||||
let status = chat.serverInfo.networkStatus
|
||||
let status = chatModel.contactNetworkStatus(contact)
|
||||
return Image(systemName: status.imageName)
|
||||
.foregroundColor(status == .connected ? .green : .secondary)
|
||||
.font(.system(size: 12))
|
||||
@@ -337,7 +337,7 @@ struct ChatInfoView: View {
|
||||
private func networkStatusAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Network status"),
|
||||
message: Text(chat.serverInfo.networkStatus.statusExplanation)
|
||||
message: Text(chatModel.contactNetworkStatus(contact).statusExplanation)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,25 +17,28 @@ struct CIVoiceView: View {
|
||||
@State var playbackTime: TimeInterval?
|
||||
|
||||
var body: some View {
|
||||
VStack (
|
||||
alignment: chatItem.chatDir.sent ? .trailing : .leading,
|
||||
spacing: 6
|
||||
) {
|
||||
HStack {
|
||||
if chatItem.chatDir.sent {
|
||||
playerTime()
|
||||
.frame(width: 50, alignment: .leading)
|
||||
player()
|
||||
} else {
|
||||
player()
|
||||
playerTime()
|
||||
.frame(width: 50, alignment: .leading)
|
||||
Group {
|
||||
if chatItem.chatDir.sent {
|
||||
VStack (alignment: .trailing, spacing: 6) {
|
||||
HStack {
|
||||
playerTime()
|
||||
player()
|
||||
}
|
||||
.frame(alignment: .trailing)
|
||||
metaView().padding(.trailing, 10)
|
||||
}
|
||||
} else {
|
||||
VStack (alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
player()
|
||||
playerTime()
|
||||
}
|
||||
.frame(alignment: .leading)
|
||||
metaView().padding(.leading, -6)
|
||||
}
|
||||
}
|
||||
CIMetaView(chatItem: chatItem)
|
||||
.padding(.leading, chatItem.chatDir.sent ? 0 : 12)
|
||||
.padding(.trailing, chatItem.chatDir.sent ? 12 : 0)
|
||||
}
|
||||
.padding([.top, .horizontal], 4)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
@@ -58,6 +61,10 @@ struct CIVoiceView: View {
|
||||
)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
private func metaView() -> some View {
|
||||
CIMetaView(chatItem: chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
struct VoiceMessagePlayerTime: View {
|
||||
@@ -66,13 +73,16 @@ struct VoiceMessagePlayerTime: View {
|
||||
@Binding var playbackTime: TimeInterval?
|
||||
|
||||
var body: some View {
|
||||
switch playbackState {
|
||||
case .noPlayback:
|
||||
Text(voiceMessageTime(recordingTime))
|
||||
case .playing:
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
case .paused:
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
ZStack(alignment: .leading) {
|
||||
Text(String("66:66")).foregroundColor(.clear)
|
||||
switch playbackState {
|
||||
case .noPlayback:
|
||||
Text(voiceMessageTime(recordingTime))
|
||||
case .playing:
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
case .paused:
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,33 +63,42 @@ struct ChatView: View {
|
||||
.padding(.top, 1)
|
||||
.navigationTitle(cInfo.chatViewName)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.onAppear {
|
||||
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
|
||||
composeState = draft
|
||||
}
|
||||
if chat.chatStats.unreadChat {
|
||||
Task {
|
||||
await markChatUnread(chat, unreadChat: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { _ in
|
||||
if chatModel.chatId == nil { dismiss() }
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
chatModel.chatId = nil
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
|
||||
if chatModel.chatId == nil {
|
||||
chatModel.reversedChatItems = []
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "chevron.backward")
|
||||
Text("Chats")
|
||||
.onChange(of: chatModel.chatId) { _ in
|
||||
if chatModel.chatId == nil { dismiss() }
|
||||
}
|
||||
.onChange(of: "\(composeState.empty) \(composeState.noPreview) \(composeState.message)") { _ in
|
||||
if !composeState.empty {
|
||||
chatModel.draft = composeState
|
||||
chatModel.draftChatId = chat.id
|
||||
} else if chatModel.draftChatId == chat.id {
|
||||
chatModel.draft = nil
|
||||
chatModel.draftChatId = nil
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if chatModel.chatId == cInfo.id {
|
||||
chatModel.chatId = nil
|
||||
if chatModel.draftChatId == cInfo.id {
|
||||
chatModel.draft = composeState
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
|
||||
if chatModel.chatId == nil {
|
||||
chatModel.reversedChatItems = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
if case let .direct(contact) = cInfo {
|
||||
Button {
|
||||
@@ -181,7 +190,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func searchToolbar() -> some View {
|
||||
HStack {
|
||||
HStack {
|
||||
@@ -237,7 +246,6 @@ struct ChatView: View {
|
||||
if chatModel.chatId == cInfo.id && itemsInView.contains(ci.viewId) {
|
||||
Task {
|
||||
await apiMarkChatItemRead(cInfo, ci)
|
||||
NtfManager.shared.decNtfBadgeCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +161,10 @@ struct ComposeState {
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
||||
var empty: Bool {
|
||||
message == "" && liveMessage == nil && noPreview
|
||||
}
|
||||
}
|
||||
|
||||
func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
|
||||
@@ -549,14 +553,16 @@ struct ComposeView: View {
|
||||
sent = await send(checkLinkPreview(), quoted: quoted, live: live)
|
||||
case let .imagePreviews(imagePreviews: images):
|
||||
let last = min(chosenImages.count, images.count) - 1
|
||||
for i in 0..<last {
|
||||
if let savedFile = saveAnyImage(chosenImages[i]) {
|
||||
_ = await send(.image(text: "", image: images[i]), quoted: nil, file: savedFile)
|
||||
if last >= 0 {
|
||||
for i in 0..<last {
|
||||
if let savedFile = saveAnyImage(chosenImages[i]) {
|
||||
_ = await send(.image(text: "", image: images[i]), quoted: nil, file: savedFile)
|
||||
}
|
||||
_ = try? await Task.sleep(nanoseconds: 100_000000)
|
||||
}
|
||||
if let savedFile = saveAnyImage(chosenImages[last]) {
|
||||
sent = await send(.image(text: msgText, image: images[last]), quoted: quoted, file: savedFile, live: live)
|
||||
}
|
||||
_ = try? await Task.sleep(nanoseconds: 100_000000)
|
||||
}
|
||||
if let savedFile = saveAnyImage(chosenImages[last]) {
|
||||
sent = await send(.image(text: msgText, image: images[last]), quoted: quoted, file: savedFile, live: live)
|
||||
}
|
||||
if sent == nil {
|
||||
sent = await send(.text(msgText), quoted: quoted, live: live)
|
||||
|
||||
@@ -16,13 +16,11 @@ enum VoiceMessagePlaybackState {
|
||||
}
|
||||
|
||||
func voiceMessageTime(_ time: TimeInterval) -> String {
|
||||
let min = Int(time / 60)
|
||||
let sec = Int(time.truncatingRemainder(dividingBy: 60))
|
||||
return String(format: "%02d:%02d", min, sec)
|
||||
durationText(Int(time))
|
||||
}
|
||||
|
||||
func voiceMessageTime_(_ time: TimeInterval?) -> String {
|
||||
return voiceMessageTime(time ?? TimeInterval(0))
|
||||
durationText(Int(time ?? 0))
|
||||
}
|
||||
|
||||
struct ComposeVoiceView: View {
|
||||
|
||||
@@ -76,45 +76,20 @@ struct SendMessageView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if (composeState.inProgress) {
|
||||
if composeState.inProgress {
|
||||
ProgressView()
|
||||
.scaleEffect(1.4)
|
||||
.frame(width: 31, height: 31, alignment: .center)
|
||||
.padding([.bottom, .trailing], 3)
|
||||
} else {
|
||||
let vmrs = composeState.voiceMessageRecordingState
|
||||
if showVoiceMessageButton
|
||||
&& composeState.message.isEmpty
|
||||
&& !composeState.editing
|
||||
&& composeState.liveMessage == nil
|
||||
&& ((composeState.noPreview && vmrs == .noRecording)
|
||||
|| (vmrs == .recording && holdingVMR)) {
|
||||
HStack {
|
||||
if voiceMessageAllowed {
|
||||
RecordVoiceMessageButton(
|
||||
startVoiceMessageRecording: startVoiceMessageRecording,
|
||||
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
||||
holdingVMR: $holdingVMR,
|
||||
disabled: composeState.disabled
|
||||
)
|
||||
} else {
|
||||
voiceMessageNotAllowedButton()
|
||||
}
|
||||
if let send = sendLiveMessage,
|
||||
let update = updateLiveMessage,
|
||||
case .noContextItem = composeState.contextItem {
|
||||
startLiveMessageButton(send: send, update: update)
|
||||
}
|
||||
VStack(alignment: .trailing) {
|
||||
if teHeight > 100 {
|
||||
deleteTextButton()
|
||||
Spacer()
|
||||
}
|
||||
} else if vmrs == .recording && !holdingVMR {
|
||||
finishVoiceMessageRecordingButton()
|
||||
} else if composeState.liveMessage != nil && composeState.liveMessage?.sentMsg == nil && composeState.message.isEmpty {
|
||||
cancelLiveMessageButton {
|
||||
cancelLiveMessage?()
|
||||
}
|
||||
} else {
|
||||
sendMessageButton()
|
||||
composeActionButtons()
|
||||
}
|
||||
.frame(height: teHeight, alignment: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +100,52 @@ struct SendMessageView: View {
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
@ViewBuilder private func composeActionButtons() -> some View {
|
||||
let vmrs = composeState.voiceMessageRecordingState
|
||||
if showVoiceMessageButton
|
||||
&& composeState.message.isEmpty
|
||||
&& !composeState.editing
|
||||
&& composeState.liveMessage == nil
|
||||
&& ((composeState.noPreview && vmrs == .noRecording)
|
||||
|| (vmrs == .recording && holdingVMR)) {
|
||||
HStack {
|
||||
if voiceMessageAllowed {
|
||||
RecordVoiceMessageButton(
|
||||
startVoiceMessageRecording: startVoiceMessageRecording,
|
||||
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
||||
holdingVMR: $holdingVMR,
|
||||
disabled: composeState.disabled
|
||||
)
|
||||
} else {
|
||||
voiceMessageNotAllowedButton()
|
||||
}
|
||||
if let send = sendLiveMessage,
|
||||
let update = updateLiveMessage,
|
||||
case .noContextItem = composeState.contextItem {
|
||||
startLiveMessageButton(send: send, update: update)
|
||||
}
|
||||
}
|
||||
} else if vmrs == .recording && !holdingVMR {
|
||||
finishVoiceMessageRecordingButton()
|
||||
} else if composeState.liveMessage != nil && composeState.liveMessage?.sentMsg == nil && composeState.message.isEmpty {
|
||||
cancelLiveMessageButton {
|
||||
cancelLiveMessage?()
|
||||
}
|
||||
} else {
|
||||
sendMessageButton()
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteTextButton() -> some View {
|
||||
Button {
|
||||
composeState.message = ""
|
||||
} label: {
|
||||
Image(systemName: "multiply.circle.fill")
|
||||
}
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
.padding([.top, .trailing], 4)
|
||||
}
|
||||
|
||||
@ViewBuilder private func sendMessageButton() -> some View {
|
||||
let v = Button(action: sendMessage) {
|
||||
Image(systemName: composeState.editing || composeState.liveMessage != nil
|
||||
|
||||
@@ -145,12 +145,6 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func serverImage() -> some View {
|
||||
let status = chat.serverInfo.networkStatus
|
||||
return Image(systemName: status.imageName)
|
||||
.foregroundColor(status == .connected ? .green : .secondary)
|
||||
}
|
||||
|
||||
private func memberView(_ member: GroupMember, user: Bool = false) -> some View {
|
||||
HStack{
|
||||
ProfileImage(imageStr: member.image)
|
||||
|
||||
@@ -155,8 +155,6 @@ struct GroupMemberInfoView: View {
|
||||
Button {
|
||||
do {
|
||||
let chat = try apiGetChat(type: .direct, id: contactId)
|
||||
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend
|
||||
chat.serverInfo = Chat.ServerInfo(networkStatus: .connected)
|
||||
chatModel.addChat(chat)
|
||||
dismissAllSheets(animated: true)
|
||||
DispatchQueue.main.async {
|
||||
|
||||
@@ -416,9 +416,9 @@ struct ErrorAlert {
|
||||
|
||||
func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert {
|
||||
switch error as? ChatResponse {
|
||||
case let .chatCmdError(.errorAgent(.BROKER(addr, .TIMEOUT))):
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
|
||||
return ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
|
||||
case let .chatCmdError(.errorAgent(.BROKER(addr, .NETWORK))):
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))):
|
||||
return ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
|
||||
default:
|
||||
return ErrorAlert(title: title, message: "Error: \(responseError(error))")
|
||||
|
||||
@@ -11,29 +11,39 @@ import SimpleXChat
|
||||
|
||||
struct ChatListView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
// not really used in this view
|
||||
@State private var showSettings = false
|
||||
@State private var searchText = ""
|
||||
@State private var showAddChat = false
|
||||
@State var userPickerVisible = false
|
||||
|
||||
var body: some View {
|
||||
NavStackCompat(
|
||||
isActive: Binding(
|
||||
get: { ChatModel.shared.chatId != nil },
|
||||
set: { _ in }
|
||||
),
|
||||
destination: chatView
|
||||
) {
|
||||
VStack {
|
||||
if chatModel.chats.isEmpty {
|
||||
onboardingButtons()
|
||||
}
|
||||
if chatModel.chats.count > 8 {
|
||||
chatList.searchable(text: $searchText)
|
||||
} else {
|
||||
chatList
|
||||
ZStack(alignment: .topLeading) {
|
||||
NavStackCompat(
|
||||
isActive: Binding(
|
||||
get: { ChatModel.shared.chatId != nil },
|
||||
set: { _ in }
|
||||
),
|
||||
destination: chatView
|
||||
) {
|
||||
VStack {
|
||||
if chatModel.chats.isEmpty {
|
||||
onboardingButtons()
|
||||
}
|
||||
if chatModel.chats.count > 8 {
|
||||
chatList.searchable(text: $searchText)
|
||||
} else {
|
||||
chatList
|
||||
}
|
||||
}
|
||||
}
|
||||
if userPickerVisible {
|
||||
Rectangle().fill(.white.opacity(0.001)).onTapGesture {
|
||||
withAnimation {
|
||||
userPickerVisible.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
UserPicker(showSettings: $showSettings, userPickerVisible: $userPickerVisible)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,13 +63,35 @@ struct ChatListView: View {
|
||||
}
|
||||
.onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() }
|
||||
.onAppear() { connectViaUrl() }
|
||||
.onDisappear() { withAnimation { userPickerVisible = false } }
|
||||
.offset(x: -8)
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Your chats")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
SettingsButton()
|
||||
Button {
|
||||
if chatModel.users.count > 1 {
|
||||
withAnimation {
|
||||
userPickerVisible.toggle()
|
||||
}
|
||||
} else {
|
||||
showSettings = true
|
||||
}
|
||||
} label: {
|
||||
let user = chatModel.currentUser ?? User.sampleData
|
||||
ZStack(alignment: .topTrailing) {
|
||||
ProfileImage(imageStr: user.image, color: Color(uiColor: .quaternaryLabel))
|
||||
.frame(width: 32, height: 32)
|
||||
.padding(.trailing, 4)
|
||||
let allRead = chatModel.users
|
||||
.filter { !$0.user.activeUser }
|
||||
.allSatisfy { u in u.unreadCount == 0 }
|
||||
if !allRead {
|
||||
unreadBadge(size: 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
if (chatModel.incognito) {
|
||||
@@ -70,6 +102,8 @@ struct ChatListView: View {
|
||||
}
|
||||
Image(systemName: "theatermasks").frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.indigo)
|
||||
}
|
||||
} else {
|
||||
Text("Your chats").font(.headline)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
@@ -80,6 +114,15 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView(showSettings: $showSettings)
|
||||
}
|
||||
}
|
||||
|
||||
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
|
||||
Circle()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
private func onboardingButtons() -> some View {
|
||||
|
||||
@@ -106,31 +106,57 @@ struct ChatPreviewView: View {
|
||||
.kerning(-2)
|
||||
}
|
||||
|
||||
private func chatPreviewLayout(_ text: Text) -> some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
text
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
.padding(.leading, 8)
|
||||
.padding(.trailing, 36)
|
||||
let s = chat.chatStats
|
||||
if s.unreadCount > 0 || s.unreadChat {
|
||||
unreadCountText(s.unreadCount)
|
||||
.font(.caption)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 4)
|
||||
.frame(minWidth: 18, minHeight: 18)
|
||||
.background(chat.chatInfo.ntfsEnabled ? Color.accentColor : Color.secondary)
|
||||
.cornerRadius(10)
|
||||
} else if !chat.chatInfo.ntfsEnabled {
|
||||
Image(systemName: "speaker.slash.fill")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func messageDraft(_ draft: ComposeState) -> Text {
|
||||
let msg = draft.message
|
||||
return image("rectangle.and.pencil.and.ellipsis", color: .accentColor)
|
||||
+ attachment()
|
||||
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true)
|
||||
|
||||
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
|
||||
Text(Image(systemName: s)).foregroundColor(color) + Text(" ")
|
||||
}
|
||||
|
||||
func attachment() -> Text {
|
||||
switch draft.preview {
|
||||
case .filePreview: return image("doc.fill")
|
||||
case .imagePreviews: return image("photo")
|
||||
case let .voicePreview(_, duration): return image("play.fill") + Text(voiceMessageTime(TimeInterval(duration)))
|
||||
default: return Text("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatPreviewText(_ cItem: ChatItem?) -> some View {
|
||||
if let cItem = cItem {
|
||||
if chatModel.draftChatId == chat.id, let draft = chatModel.draft {
|
||||
chatPreviewLayout(messageDraft(draft))
|
||||
} else if let cItem = cItem {
|
||||
let itemText = !cItem.meta.itemDeleted ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
|
||||
let itemFormattedText = !cItem.meta.itemDeleted ? cItem.formattedText : nil
|
||||
ZStack(alignment: .topTrailing) {
|
||||
(itemStatusMark(cItem) + messageText(itemText, itemFormattedText, cItem.memberDisplayName, preview: true))
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
.padding(.leading, 8)
|
||||
.padding(.trailing, 36)
|
||||
let s = chat.chatStats
|
||||
if s.unreadCount > 0 || s.unreadChat {
|
||||
unreadCountText(s.unreadCount)
|
||||
.font(.caption)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 4)
|
||||
.frame(minWidth: 18, minHeight: 18)
|
||||
.background(chat.chatInfo.ntfsEnabled ? Color.accentColor : Color.secondary)
|
||||
.cornerRadius(10)
|
||||
} else if !chat.chatInfo.ntfsEnabled {
|
||||
Image(systemName: "speaker.slash.fill")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
chatPreviewLayout(itemStatusMark(cItem) + messageText(itemText, itemFormattedText, cItem.memberDisplayName, preview: true))
|
||||
} else {
|
||||
switch (chat.chatInfo) {
|
||||
case let .direct(contact):
|
||||
@@ -179,16 +205,21 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatStatusImage() -> some View {
|
||||
switch chat.serverInfo.networkStatus {
|
||||
case .connected: EmptyView()
|
||||
case .error:
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 17, height: 17)
|
||||
.foregroundColor(.secondary)
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact):
|
||||
switch (chatModel.contactNetworkStatus(contact)) {
|
||||
case .connected: EmptyView()
|
||||
case .error:
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 17, height: 17)
|
||||
.foregroundColor(.secondary)
|
||||
default:
|
||||
ProgressView()
|
||||
}
|
||||
default:
|
||||
ProgressView()
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,8 +251,7 @@ struct ChatPreviewView_Previews: PreviewProvider {
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)],
|
||||
chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0),
|
||||
serverInfo: Chat.ServerInfo(networkStatus: .error("status"))
|
||||
chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0)
|
||||
))
|
||||
ChatPreviewView(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.group,
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
//
|
||||
// Created by Avently on 16.01.2023.
|
||||
// Copyright (c) 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
private let fillColorDark = Color(uiColor: UIColor(red: 0.11, green: 0.11, blue: 0.11, alpha: 255))
|
||||
private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 255))
|
||||
|
||||
struct UserPicker: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var userPickerVisible: Bool
|
||||
@State var scrollViewContentSize: CGSize = .zero
|
||||
@State var disableScrolling: Bool = true
|
||||
private let menuButtonHeight: CGFloat = 68
|
||||
@State var chatViewNameWidth: CGFloat = 0
|
||||
|
||||
var fillColor: Color {
|
||||
colorScheme == .dark ? fillColorDark : fillColorLight
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer().frame(height: 1)
|
||||
VStack(spacing: 0) {
|
||||
ScrollView {
|
||||
ScrollViewReader { sp in
|
||||
let users = m.users.sorted { u, _ in u.user.activeUser }
|
||||
VStack(spacing: 0) {
|
||||
ForEach(users) { u in
|
||||
userView(u)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
GeometryReader { geo -> Color in
|
||||
DispatchQueue.main.async {
|
||||
scrollViewContentSize = geo.size
|
||||
let scenes = UIApplication.shared.connectedScenes
|
||||
if let windowScene = scenes.first as? UIWindowScene {
|
||||
let layoutFrame = windowScene.windows[0].safeAreaLayoutGuide.layoutFrame
|
||||
disableScrolling = scrollViewContentSize.height + menuButtonHeight + 10 < layoutFrame.height
|
||||
}
|
||||
}
|
||||
return Color.clear
|
||||
}
|
||||
}
|
||||
.onChange(of: userPickerVisible) { visible in
|
||||
if visible, let u = users.first {
|
||||
sp.scrollTo(u.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000))
|
||||
.frame(maxHeight: scrollViewContentSize.height)
|
||||
|
||||
menuButton("Settings", icon: "gearshape") {
|
||||
showSettings = true
|
||||
withAnimation {
|
||||
userPickerVisible.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(fillColor)
|
||||
.cornerRadius(16)
|
||||
.shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0)
|
||||
)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { chatViewNameWidth = $0 }
|
||||
.frame(maxWidth: chatViewNameWidth > 0 ? min(300, chatViewNameWidth + 130) : 300)
|
||||
.padding(8)
|
||||
.opacity(userPickerVisible ? 1.0 : 0.0)
|
||||
.onAppear {
|
||||
do {
|
||||
m.users = try listUsers()
|
||||
} catch let error {
|
||||
logger.error("Error updating users \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func userView(_ u: UserInfo) -> some View {
|
||||
let user = u.user
|
||||
return Button(action: {
|
||||
if !user.activeUser {
|
||||
changeActiveUser(user.userId)
|
||||
userPickerVisible = false
|
||||
}
|
||||
}, label: {
|
||||
HStack(spacing: 0) {
|
||||
ProfileImage(imageStr: user.image, color: Color(uiColor: .tertiarySystemFill))
|
||||
.frame(width: 44, height: 44)
|
||||
.padding(.trailing, 12)
|
||||
Text(user.chatViewName)
|
||||
.fontWeight(user.activeUser ? .medium : .regular)
|
||||
.foregroundColor(.primary)
|
||||
.overlay(DetermineWidth())
|
||||
Spacer()
|
||||
if user.activeUser {
|
||||
Image(systemName: "checkmark")
|
||||
} else if u.unreadCount > 0 {
|
||||
unreadCounter(u.unreadCount)
|
||||
}
|
||||
}
|
||||
.padding(.trailing)
|
||||
.padding([.leading, .vertical], 12)
|
||||
})
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
}
|
||||
|
||||
private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 0) {
|
||||
Text(title)
|
||||
.overlay(DetermineWidth())
|
||||
Spacer()
|
||||
Image(systemName: icon)
|
||||
// .frame(width: 24, alignment: .center)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 22)
|
||||
.frame(height: menuButtonHeight)
|
||||
}
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
}
|
||||
}
|
||||
|
||||
func unreadCounter(_ unread: Int) -> some View {
|
||||
unreadCountText(unread)
|
||||
.font(.caption)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 4)
|
||||
.frame(minWidth: 18, minHeight: 18)
|
||||
.background(Color.accentColor)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
struct UserPicker_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let m = ChatModel()
|
||||
m.users = [UserInfo.sampleData, UserInfo.sampleData]
|
||||
return UserPicker(
|
||||
showSettings: Binding.constant(false),
|
||||
userPickerVisible: Binding.constant(true)
|
||||
)
|
||||
.environmentObject(m)
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ struct DatabaseEncryptionView: View {
|
||||
await operationEnded(.databaseEncrypted)
|
||||
}
|
||||
} catch let error {
|
||||
if case .chatCmdError(.errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
|
||||
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
|
||||
await operationEnded(.currentPassphraseError)
|
||||
} else {
|
||||
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))
|
||||
|
||||
@@ -67,6 +67,23 @@ struct DatabaseView: View {
|
||||
private func chatDatabaseView() -> some View {
|
||||
List {
|
||||
let stopped = m.chatRunning == false
|
||||
Section {
|
||||
Picker("Delete messages after", selection: $chatItemTTL) {
|
||||
ForEach(ChatItemTTL.values) { ttl in
|
||||
Text(ttl.deleteAfterText).tag(ttl)
|
||||
}
|
||||
if case .seconds = chatItemTTL {
|
||||
Text(chatItemTTL.deleteAfterText).tag(chatItemTTL)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
.disabled(m.chatDbChanged || progressIndicator)
|
||||
} header: {
|
||||
Text("Messages")
|
||||
} footer: {
|
||||
Text("This setting applies to messages in your current chat profile **\(m.currentUser?.displayName ?? "")**.")
|
||||
}
|
||||
|
||||
Section {
|
||||
settingsRow(
|
||||
stopped ? "exclamationmark.octagon.fill" : "play.fill",
|
||||
@@ -157,22 +174,12 @@ struct DatabaseView: View {
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Delete messages after", selection: $chatItemTTL) {
|
||||
ForEach(ChatItemTTL.values) { ttl in
|
||||
Text(ttl.deleteAfterText).tag(ttl)
|
||||
}
|
||||
if case .seconds = chatItemTTL {
|
||||
Text(chatItemTTL.deleteAfterText).tag(chatItemTTL)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
.disabled(m.chatDbChanged || progressIndicator)
|
||||
Button("Delete files & media", role: .destructive) {
|
||||
Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) {
|
||||
alert = .deleteFilesAndMedia
|
||||
}
|
||||
.disabled(!stopped || appFilesCountAndSize?.0 == 0)
|
||||
} header: {
|
||||
Text("Data")
|
||||
Text("Files & media")
|
||||
} footer: {
|
||||
if let (fileCount, size) = appFilesCountAndSize {
|
||||
if fileCount == 0 {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Created by Avently on 16.01.2023.
|
||||
// Copyright (c) 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PressedButtonStyle: ButtonStyle {
|
||||
var defaultColor: Color
|
||||
var pressedColor: Color
|
||||
func makeBody(configuration: Self.Configuration) -> some View {
|
||||
configuration.label
|
||||
.background(configuration.isPressed ? pressedColor : defaultColor)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CreateProfile: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State private var displayName: String = ""
|
||||
@State private var fullName: String = ""
|
||||
@FocusState private var focusDisplayName
|
||||
@@ -50,13 +51,17 @@ struct CreateProfile: View {
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Button {
|
||||
hideKeyboard()
|
||||
withAnimation { m.onboardingStage = .step1_SimpleXInfo }
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "lessthan")
|
||||
Text("About SimpleX")
|
||||
if m.users.isEmpty {
|
||||
Button {
|
||||
hideKeyboard()
|
||||
withAnimation {
|
||||
m.onboardingStage = .step1_SimpleXInfo
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "lessthan")
|
||||
Text("About SimpleX")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,9 +101,14 @@ struct CreateProfile: View {
|
||||
)
|
||||
do {
|
||||
m.currentUser = try apiCreateActiveUser(profile)
|
||||
try startChat()
|
||||
withAnimation { m.onboardingStage = .step3_SetNotificationsMode }
|
||||
|
||||
if m.users.isEmpty {
|
||||
try startChat()
|
||||
withAnimation { m.onboardingStage = .step3_SetNotificationsMode }
|
||||
} else {
|
||||
dismiss()
|
||||
m.users = try listUsers()
|
||||
try getUserChatData()
|
||||
}
|
||||
} catch {
|
||||
fatalError("Failed to create user or start chat: \(responseError(error))")
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ struct TerminalView: View {
|
||||
func sendMessage() {
|
||||
let cmd = ChatCommand.string(composeState.message)
|
||||
if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) {
|
||||
let resp = ChatResponse.chatCmdError(chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
|
||||
let resp = ChatResponse.chatCmdError(user: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.terminalItems.append(.cmd(.now, cmd))
|
||||
ChatModel.shared.terminalItems.append(.resp(.now, resp))
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
enum OnionHostsAlert: Identifiable {
|
||||
case update(hosts: OnionHosts)
|
||||
private enum NetworkAlert: Identifiable {
|
||||
case updateOnionHosts(hosts: OnionHosts)
|
||||
case updateSessionMode(mode: TransportSessionMode)
|
||||
case error(err: String)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .update(hosts): return "update \(hosts)"
|
||||
case let .updateOnionHosts(hosts): return "updateOnionHosts \(hosts)"
|
||||
case let .updateSessionMode(mode): return "updateSessionMode \(mode)"
|
||||
case let .error(err): return "error \(err)"
|
||||
}
|
||||
}
|
||||
@@ -27,7 +29,8 @@ struct NetworkAndServers: View {
|
||||
@State private var currentNetCfg = NetCfg.defaults
|
||||
@State private var netCfg = NetCfg.defaults
|
||||
@State private var onionHosts: OnionHosts = .no
|
||||
@State private var showOnionHostsAlert: OnionHostsAlert?
|
||||
@State private var sessionMode: TransportSessionMode = .user
|
||||
@State private var alert: NetworkAlert?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@@ -45,6 +48,12 @@ struct NetworkAndServers: View {
|
||||
}
|
||||
.frame(height: 36)
|
||||
|
||||
if developerTools {
|
||||
Picker("Transport isolation", selection: $sessionMode) {
|
||||
ForEach(TransportSessionMode.values, id: \.self) { Text($0.text) }
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
AdvancedNetworkSettings()
|
||||
.navigationTitle("Network settings")
|
||||
@@ -75,17 +84,37 @@ struct NetworkAndServers: View {
|
||||
}
|
||||
.onChange(of: onionHosts) { _ in
|
||||
if onionHosts != OnionHosts(netCfg: currentNetCfg) {
|
||||
showOnionHostsAlert = .update(hosts: onionHosts)
|
||||
alert = .updateOnionHosts(hosts: onionHosts)
|
||||
}
|
||||
}
|
||||
.alert(item: $showOnionHostsAlert) { a in
|
||||
.onChange(of: sessionMode) { _ in
|
||||
if sessionMode != netCfg.sessionMode {
|
||||
alert = .updateSessionMode(mode: sessionMode)
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { a in
|
||||
switch a {
|
||||
case let .update(hosts):
|
||||
case let .updateOnionHosts(hosts):
|
||||
return Alert(
|
||||
title: Text("Update .onion hosts setting?"),
|
||||
message: Text(onionHostsInfo()) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
|
||||
message: Text(onionHostsInfo(hosts)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
|
||||
primaryButton: .default(Text("Ok")) {
|
||||
saveNetCfg(hosts)
|
||||
let (hostMode, requiredHostMode) = hosts.hostMode
|
||||
netCfg.hostMode = hostMode
|
||||
netCfg.requiredHostMode = requiredHostMode
|
||||
saveNetCfg()
|
||||
},
|
||||
secondaryButton: .cancel() {
|
||||
resetNetCfgView()
|
||||
}
|
||||
)
|
||||
case let .updateSessionMode(mode):
|
||||
return Alert(
|
||||
title: Text("Update transport isolation mode?"),
|
||||
message: Text(sessionModeInfo(mode)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
|
||||
primaryButton: .default(Text("Ok")) {
|
||||
netCfg.sessionMode = mode
|
||||
saveNetCfg()
|
||||
},
|
||||
secondaryButton: .cancel() {
|
||||
resetNetCfgView()
|
||||
@@ -100,11 +129,8 @@ struct NetworkAndServers: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func saveNetCfg(_ hosts: OnionHosts) {
|
||||
private func saveNetCfg() {
|
||||
do {
|
||||
let (hostMode, requiredHostMode) = hosts.hostMode
|
||||
netCfg.hostMode = hostMode
|
||||
netCfg.requiredHostMode = requiredHostMode
|
||||
let def = netCfg.hostMode == .onionHost ? NetCfg.proxyDefaults : NetCfg.defaults
|
||||
netCfg.tcpConnectTimeout = def.tcpConnectTimeout
|
||||
netCfg.tcpTimeout = def.tcpTimeout
|
||||
@@ -114,7 +140,7 @@ struct NetworkAndServers: View {
|
||||
} catch let error {
|
||||
let err = responseError(error)
|
||||
resetNetCfgView()
|
||||
showOnionHostsAlert = .error(err: err)
|
||||
alert = .error(err: err)
|
||||
logger.error("\(err)")
|
||||
}
|
||||
}
|
||||
@@ -122,15 +148,23 @@ struct NetworkAndServers: View {
|
||||
private func resetNetCfgView() {
|
||||
netCfg = currentNetCfg
|
||||
onionHosts = OnionHosts(netCfg: netCfg)
|
||||
sessionMode = netCfg.sessionMode
|
||||
}
|
||||
|
||||
private func onionHostsInfo() -> LocalizedStringKey {
|
||||
switch onionHosts {
|
||||
private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey {
|
||||
switch hosts {
|
||||
case .no: return "Onion hosts will not be used."
|
||||
case .prefer: return "Onion hosts will be used when available. Requires enabling VPN."
|
||||
case .require: return "Onion hosts will be required for connection. Requires enabling VPN."
|
||||
}
|
||||
}
|
||||
|
||||
private func sessionModeInfo(_ mode: TransportSessionMode) -> LocalizedStringKey {
|
||||
switch mode {
|
||||
case .user: return "A separate TCP connection will be used **for each chat profile you have in the app**."
|
||||
case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NetworkServersView_Previews: PreviewProvider {
|
||||
|
||||
@@ -71,10 +71,7 @@ struct PreferencesView: View {
|
||||
p.preferences = fullPreferencesToPreferences(preferences)
|
||||
if let newProfile = try await apiUpdateProfile(profile: p) {
|
||||
await MainActor.run {
|
||||
if let profileId = chatModel.currentUser?.profile.profileId {
|
||||
chatModel.currentUser?.profile = toLocalProfile(profileId, newProfile, "")
|
||||
chatModel.currentUser?.fullPreferences = preferences
|
||||
}
|
||||
chatModel.updateCurrentUser(newProfile, preferences)
|
||||
currentPreferences = preferences
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ struct SMPServersView: View {
|
||||
|
||||
private func smpServersView() -> some View {
|
||||
List {
|
||||
Section("SMP servers") {
|
||||
Section {
|
||||
ForEach($servers) { srv in
|
||||
smpServerView(srv)
|
||||
}
|
||||
@@ -57,6 +57,11 @@ struct SMPServersView: View {
|
||||
Button("Add server…") {
|
||||
showAddServer = true
|
||||
}
|
||||
} header: {
|
||||
Text("SMP servers")
|
||||
} footer: {
|
||||
Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.")
|
||||
.lineLimit(10)
|
||||
}
|
||||
|
||||
Section {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// SettingsButton.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Evgeny Poberezkin on 31/01/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsButton: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State private var showSettings = false
|
||||
|
||||
var body: some View {
|
||||
Button { showSettings = true } label: {
|
||||
Image(systemName: "gearshape")
|
||||
}
|
||||
.sheet(isPresented: $showSettings, content: {
|
||||
SettingsView(showSettings: $showSettings)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsButton()
|
||||
}
|
||||
}
|
||||
@@ -113,12 +113,19 @@ struct SettingsView: View {
|
||||
Section("You") {
|
||||
NavigationLink {
|
||||
UserProfile()
|
||||
.navigationTitle("Your chat profile")
|
||||
.navigationTitle("Your current profile")
|
||||
} label: {
|
||||
ProfilePreview(profileOf: user)
|
||||
.padding(.leading, -8)
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
UserProfilesView()
|
||||
.navigationTitle("Your chat profiles")
|
||||
} label: {
|
||||
settingsRow("person.crop.rectangle.stack") { Text("Your chat profiles") }
|
||||
}
|
||||
|
||||
incognitoRow()
|
||||
|
||||
NavigationLink {
|
||||
@@ -387,7 +394,7 @@ func settingsRow<Content : View>(_ icon: String, color: Color = .secondary, cont
|
||||
|
||||
struct ProfilePreview: View {
|
||||
var profileOf: NamedChat
|
||||
var color = Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
var color = Color(uiColor: .tertiarySystemFill)
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
|
||||
@@ -141,9 +141,7 @@ struct UserProfile: View {
|
||||
do {
|
||||
if let newProfile = try await apiUpdateProfile(profile: profile) {
|
||||
DispatchQueue.main.async {
|
||||
if let profileId = chatModel.currentUser?.profile.profileId {
|
||||
chatModel.currentUser?.profile = toLocalProfile(profileId, newProfile, "")
|
||||
}
|
||||
chatModel.updateCurrentUser(newProfile)
|
||||
profile = newProfile
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
//
|
||||
// Created by Avently on 17.01.2023.
|
||||
// Copyright (c) 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct UserProfilesView: View {
|
||||
@EnvironmentObject private var m: ChatModel
|
||||
@Environment(\.editMode) private var editMode
|
||||
@State private var showDeleteConfirmation = false
|
||||
@State private var userToDelete: Int?
|
||||
@State private var alert: UserProfilesAlert?
|
||||
|
||||
private enum UserProfilesAlert: Identifiable {
|
||||
case deleteUser(index: Int, delSMPQueues: Bool)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .deleteUser(index, delSMPQueues): return "deleteUser \(index) \(delSMPQueues)"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
ForEach(m.users) { u in
|
||||
userView(u.user)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
if let i = indexSet.first {
|
||||
showDeleteConfirmation = true
|
||||
userToDelete = i
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
CreateProfile()
|
||||
} label: {
|
||||
Label("Add profile", systemImage: "plus")
|
||||
}
|
||||
.frame(height: 44)
|
||||
.padding(.vertical, 4)
|
||||
} footer: {
|
||||
Text("Your chat profiles are stored locally, only on your device.")
|
||||
}
|
||||
}
|
||||
.toolbar { EditButton() }
|
||||
.confirmationDialog("Delete chat profile?", isPresented: $showDeleteConfirmation, titleVisibility: .visible) {
|
||||
deleteModeButton("Profile and server connections", true)
|
||||
deleteModeButton("Local profile data only", false)
|
||||
}
|
||||
.alert(item: $alert) { alert in
|
||||
switch alert {
|
||||
case let .deleteUser(index, delSMPQueues):
|
||||
return Alert(
|
||||
title: Text("Delete user profile?"),
|
||||
message: Text("All chats and messages will be deleted - this cannot be undone!"),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
removeUser(index, delSMPQueues)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteModeButton(_ title: LocalizedStringKey, _ delSMPQueues: Bool) -> some View {
|
||||
Button(title, role: .destructive) {
|
||||
if let i = userToDelete {
|
||||
alert = .deleteUser(index: i, delSMPQueues: delSMPQueues)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeUser(_ index: Int, _ delSMPQueues: Bool) {
|
||||
if index >= m.users.count { return }
|
||||
do {
|
||||
let u = m.users[index].user
|
||||
if u.activeUser {
|
||||
if let newActive = m.users.first(where: { !$0.user.activeUser }) {
|
||||
try changeActiveUser_(newActive.user.userId)
|
||||
try deleteUser(u.userId)
|
||||
}
|
||||
} else {
|
||||
try deleteUser(u.userId)
|
||||
}
|
||||
} catch let error {
|
||||
let a = getErrorAlert(error, "Error deleting user profile")
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
|
||||
func deleteUser(_ userId: Int64) throws {
|
||||
try apiDeleteUser(userId, delSMPQueues)
|
||||
m.users.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func userView(_ user: User) -> some View {
|
||||
Button {
|
||||
changeActiveUser(user.userId)
|
||||
} label: {
|
||||
HStack {
|
||||
ProfileImage(imageStr: user.image, color: Color(uiColor: .tertiarySystemFill))
|
||||
.frame(width: 44, height: 44)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.trailing, 12)
|
||||
Text(user.chatViewName)
|
||||
Spacer()
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(user.activeUser ? .primary : .clear)
|
||||
}
|
||||
}
|
||||
.disabled(user.activeUser)
|
||||
.foregroundColor(.primary)
|
||||
.deleteDisabled(m.users.count <= 1)
|
||||
}
|
||||
}
|
||||
|
||||
struct UserProfilesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UserProfilesView()
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
|
||||
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)")
|
||||
if let connEntity = ntfMsgInfo.connEntity {
|
||||
setBestAttemptNtf(createConnectionEventNtf(connEntity))
|
||||
setBestAttemptNtf(createConnectionEventNtf(ntfMsgInfo.user, connEntity))
|
||||
if let id = connEntity.id {
|
||||
Task {
|
||||
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)")
|
||||
@@ -118,9 +118,9 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
await PendingNtfs.shared.readStream(id, for: self, msgCount: ntfMsgInfo.ntfMessages.count)
|
||||
deliverBestAttemptNtf()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
} else {
|
||||
setBestAttemptNtf(createErrorNtf(dbStatus))
|
||||
}
|
||||
@@ -209,15 +209,18 @@ func chatRecvMsg() async -> ChatResponse? {
|
||||
func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotificationContent)? {
|
||||
logger.debug("NotificationService processReceivedMsg: \(res.responseType)")
|
||||
switch res {
|
||||
case let .contactConnected(contact, _):
|
||||
return (contact.id, createContactConnectedNtf(contact))
|
||||
case let .contactConnected(user, contact, _):
|
||||
return (contact.id, createContactConnectedNtf(user, contact))
|
||||
// case let .contactConnecting(contact):
|
||||
// TODO profile update
|
||||
case let .receivedContactRequest(contactRequest):
|
||||
return (UserContact(contactRequest: contactRequest).id, createContactRequestNtf(contactRequest))
|
||||
case let .newChatItem(aChatItem):
|
||||
case let .receivedContactRequest(user, contactRequest):
|
||||
return (UserContact(contactRequest: contactRequest).id, createContactRequestNtf(user, contactRequest))
|
||||
case let .newChatItem(user, aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
var cItem = aChatItem.chatItem
|
||||
if !cInfo.ntfsEnabled {
|
||||
ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1))
|
||||
}
|
||||
if case .image = cItem.content.msgContent {
|
||||
if let file = cItem.file,
|
||||
file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV,
|
||||
@@ -234,7 +237,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotification
|
||||
cItem = apiReceiveFile(fileId: file.fileId, inline: inline)?.chatItem ?? cItem
|
||||
}
|
||||
}
|
||||
return cItem.showMutableNotification ? (aChatItem.chatId, createMessageReceivedNtf(cInfo, cItem)) : nil
|
||||
return cItem.showMutableNotification ? (aChatItem.chatId, createMessageReceivedNtf(user, cInfo, cItem)) : nil
|
||||
case let .callInvitation(invitation):
|
||||
return (invitation.contact.id, createCallInvitationNtf(invitation))
|
||||
default:
|
||||
@@ -258,10 +261,10 @@ func updateNetCfg() {
|
||||
|
||||
func apiGetActiveUser() -> User? {
|
||||
let r = sendSimpleXCmd(.showActiveUser)
|
||||
logger.debug("apiGetActiveUser sendSimpleXCmd responce: \(String(describing: r))")
|
||||
logger.debug("apiGetActiveUser sendSimpleXCmd response: \(String(describing: r))")
|
||||
switch r {
|
||||
case let .activeUser(user): return user
|
||||
case .chatCmdError(.error(.noActiveUser)): return nil
|
||||
case .chatCmdError(_, .error(.noActiveUser)): return nil
|
||||
default:
|
||||
logger.error("NotificationService apiGetActiveUser unexpected response: \(String(describing: r))")
|
||||
return nil
|
||||
@@ -290,17 +293,24 @@ func apiSetIncognito(incognito: Bool) throws {
|
||||
}
|
||||
|
||||
func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
||||
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
|
||||
if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r {
|
||||
return NtfMessages(connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages)
|
||||
guard apiGetActiveUser() != nil else {
|
||||
logger.debug("no active user")
|
||||
return nil
|
||||
}
|
||||
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
|
||||
if case let .ntfMessages(user, connEntity, msgTs, ntfMessages) = r, let user = user {
|
||||
return NtfMessages(user: user, connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages)
|
||||
} else if case let .chatCmdError(_, error) = r {
|
||||
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
|
||||
} else {
|
||||
logger.debug("apiGetNtfMessage ignored response: \(r.responseType, privacy: .public) \(String.init(describing: r), privacy: .private)")
|
||||
}
|
||||
logger.debug("apiGetNtfMessage ignored response: \(String.init(describing: r), privacy: .public)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiReceiveFile(fileId: Int64, inline: Bool) -> AChatItem? {
|
||||
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, inline: inline))
|
||||
if case let .rcvFileAccepted(chatItem) = r { return chatItem }
|
||||
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
||||
logger.error("receiveFile error: \(responseError(r))")
|
||||
return nil
|
||||
}
|
||||
@@ -312,6 +322,7 @@ func setNetworkConfig(_ cfg: NetCfg) throws {
|
||||
}
|
||||
|
||||
struct NtfMessages {
|
||||
var user: User
|
||||
var connEntity: ConnectionEntity?
|
||||
var msgTs: Date?
|
||||
var ntfMessages: [NtfMsgInfo]
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1841538E296606C74533367C /* UserPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415835CBD939A9ABDC108A /* UserPicker.swift */; };
|
||||
1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415845648CA4F5A8BCA272 /* UserProfilesView.swift */; };
|
||||
1841594C978674A7B42EF0C0 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841511920742C6E152E469F /* AnimatedImageView.swift */; };
|
||||
18415B0585EB5A9A0A7CA8CD /* PressedButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */; };
|
||||
3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C714776281C081000CB4D4B /* WebRTCView.swift */; };
|
||||
3C71477A281C0F6800CB4D4B /* www in Resources */ = {isa = PBXBuildFile; fileRef = 3C714779281C0F6800CB4D4B /* www */; };
|
||||
3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */; };
|
||||
@@ -38,6 +41,11 @@
|
||||
5C3F1D5A2844B4DE00EC8A82 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */; };
|
||||
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; };
|
||||
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
|
||||
5C54F6F2297DF8A40054C4E2 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C54F6ED297DF8A40054C4E2 /* libffi.a */; };
|
||||
5C54F6F3297DF8A40054C4E2 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C54F6EE297DF8A40054C4E2 /* libgmp.a */; };
|
||||
5C54F6F4297DF8A40054C4E2 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C54F6EF297DF8A40054C4E2 /* libgmpxx.a */; };
|
||||
5C54F6F5297DF8A40054C4E2 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C54F6F0297DF8A40054C4E2 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r-ghc8.10.7.a */; };
|
||||
5C54F6F6297DF8A40054C4E2 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C54F6F1297DF8A40054C4E2 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r.a */; };
|
||||
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
|
||||
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; };
|
||||
5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */; };
|
||||
@@ -48,11 +56,6 @@
|
||||
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */; };
|
||||
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; };
|
||||
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; };
|
||||
5C65F33C297D3D9700B67AF3 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C65F337297D3D9700B67AF3 /* libgmpxx.a */; };
|
||||
5C65F33D297D3D9700B67AF3 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C65F338297D3D9700B67AF3 /* libgmp.a */; };
|
||||
5C65F33E297D3D9700B67AF3 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C65F339297D3D9700B67AF3 /* libffi.a */; };
|
||||
5C65F33F297D3D9700B67AF3 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C65F33A297D3D9700B67AF3 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r-ghc8.10.7.a */; };
|
||||
5C65F340297D3D9700B67AF3 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C65F33B297D3D9700B67AF3 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r.a */; };
|
||||
5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65F341297D3F3600B67AF3 /* VersionView.swift */; };
|
||||
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; };
|
||||
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6BA666289BD954009B8ECC /* DismissSheets.swift */; };
|
||||
@@ -96,7 +99,6 @@
|
||||
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; };
|
||||
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; };
|
||||
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */; };
|
||||
5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D327A853F100ACCCDD /* SettingsButton.swift */; };
|
||||
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; };
|
||||
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; };
|
||||
5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.swift */; };
|
||||
@@ -224,6 +226,9 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1841511920742C6E152E469F /* AnimatedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImageView.swift; sourceTree = "<group>"; };
|
||||
18415835CBD939A9ABDC108A /* UserPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPicker.swift; sourceTree = "<group>"; };
|
||||
18415845648CA4F5A8BCA272 /* UserProfilesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserProfilesView.swift; sourceTree = "<group>"; };
|
||||
18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PressedButtonStyle.swift; sourceTree = "<group>"; };
|
||||
3C714776281C081000CB4D4B /* WebRTCView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCView.swift; sourceTree = "<group>"; };
|
||||
3C714779281C0F6800CB4D4B /* www */ = {isa = PBXFileReference; lastKnownFileType = folder; name = www; path = ../android/app/src/main/assets/www; sourceTree = "<group>"; };
|
||||
3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteToConnectView.swift; sourceTree = "<group>"; };
|
||||
@@ -256,6 +261,11 @@
|
||||
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
|
||||
5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; };
|
||||
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
|
||||
5C54F6ED297DF8A40054C4E2 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C54F6EE297DF8A40054C4E2 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C54F6EF297DF8A40054C4E2 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C54F6F0297DF8A40054C4E2 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5C54F6F1297DF8A40054C4E2 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r.a"; sourceTree = "<group>"; };
|
||||
5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; };
|
||||
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; };
|
||||
5C55A922283CEDE600C4E99E /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = "<group>"; };
|
||||
@@ -267,11 +277,6 @@
|
||||
5C5E5D3C282447AB00B0488A /* CallTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallTypes.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>"; };
|
||||
5C65F337297D3D9700B67AF3 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C65F338297D3D9700B67AF3 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C65F339297D3D9700B67AF3 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C65F33A297D3D9700B67AF3 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5C65F33B297D3D9700B67AF3 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r.a"; sourceTree = "<group>"; };
|
||||
5C65F341297D3F3600B67AF3 /* VersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionView.swift; sourceTree = "<group>"; };
|
||||
5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; };
|
||||
5C6BA666289BD954009B8ECC /* DismissSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissSheets.swift; sourceTree = "<group>"; };
|
||||
@@ -324,7 +329,6 @@
|
||||
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; };
|
||||
5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
|
||||
5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushEnvironment.swift; sourceTree = "<group>"; };
|
||||
5CB924D327A853F100ACCCDD /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = "<group>"; };
|
||||
5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = "<group>"; };
|
||||
5CB924E327A8683A00ACCCDD /* UserAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddress.swift; sourceTree = "<group>"; };
|
||||
@@ -428,12 +432,12 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C65F33C297D3D9700B67AF3 /* libgmpxx.a in Frameworks */,
|
||||
5C65F340297D3D9700B67AF3 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5C65F33D297D3D9700B67AF3 /* libgmp.a in Frameworks */,
|
||||
5C65F33F297D3D9700B67AF3 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r-ghc8.10.7.a in Frameworks */,
|
||||
5C65F33E297D3D9700B67AF3 /* libffi.a in Frameworks */,
|
||||
5C54F6F6297DF8A40054C4E2 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r.a in Frameworks */,
|
||||
5C54F6F4297DF8A40054C4E2 /* libgmpxx.a in Frameworks */,
|
||||
5C54F6F2297DF8A40054C4E2 /* libffi.a in Frameworks */,
|
||||
5C54F6F5297DF8A40054C4E2 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r-ghc8.10.7.a in Frameworks */,
|
||||
5C54F6F3297DF8A40054C4E2 /* libgmp.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -492,11 +496,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C65F339297D3D9700B67AF3 /* libffi.a */,
|
||||
5C65F338297D3D9700B67AF3 /* libgmp.a */,
|
||||
5C65F337297D3D9700B67AF3 /* libgmpxx.a */,
|
||||
5C65F33A297D3D9700B67AF3 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r-ghc8.10.7.a */,
|
||||
5C65F33B297D3D9700B67AF3 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r.a */,
|
||||
5C54F6ED297DF8A40054C4E2 /* libffi.a */,
|
||||
5C54F6EE297DF8A40054C4E2 /* libgmp.a */,
|
||||
5C54F6EF297DF8A40054C4E2 /* libgmpxx.a */,
|
||||
5C54F6F0297DF8A40054C4E2 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r-ghc8.10.7.a */,
|
||||
5C54F6F1297DF8A40054C4E2 /* libHSsimplex-chat-4.4.4-696z0wvJHN7Hvtog9MKb6r.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -540,6 +544,7 @@
|
||||
5C6BA666289BD954009B8ECC /* DismissSheets.swift */,
|
||||
5C00164328A26FBC0094D739 /* ContextMenu.swift */,
|
||||
5CA7DFC229302AF000F7FDDE /* AppSheet.swift */,
|
||||
18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */,
|
||||
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
@@ -628,7 +633,6 @@
|
||||
5CB924DF27A8678B00ACCCDD /* UserSettings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CB924D327A853F100ACCCDD /* SettingsButton.swift */,
|
||||
5CB924D627A8563F00ACCCDD /* SettingsView.swift */,
|
||||
5CB346E62868D76D001FD2EF /* NotificationsView.swift */,
|
||||
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */,
|
||||
@@ -647,6 +651,7 @@
|
||||
5CB2084E28DA4B4800D024EC /* RTCServers.swift */,
|
||||
5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */,
|
||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */,
|
||||
18415845648CA4F5A8BCA272 /* UserProfilesView.swift */,
|
||||
5C65F341297D3F3600B67AF3 /* VersionView.swift */,
|
||||
);
|
||||
path = UserSettings;
|
||||
@@ -662,6 +667,7 @@
|
||||
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */,
|
||||
5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
|
||||
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */,
|
||||
18415835CBD939A9ABDC108A /* UserPicker.swift */,
|
||||
);
|
||||
path = ChatList;
|
||||
sourceTree = "<group>";
|
||||
@@ -1061,7 +1067,6 @@
|
||||
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */,
|
||||
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */,
|
||||
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
|
||||
5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */,
|
||||
3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */,
|
||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */,
|
||||
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
|
||||
@@ -1081,6 +1086,9 @@
|
||||
644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */,
|
||||
1841594C978674A7B42EF0C0 /* AnimatedImageView.swift in Sources */,
|
||||
5C7031162953C97F00150A12 /* CIFeaturePreferenceView.swift in Sources */,
|
||||
1841538E296606C74533367C /* UserPicker.swift in Sources */,
|
||||
18415B0585EB5A9A0A7CA8CD /* PressedButtonStyle.swift in Sources */,
|
||||
1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -134,6 +134,7 @@ public func chatResponse(_ s: String) -> ChatResponse {
|
||||
type = jResp.allKeys[0] as? String
|
||||
if type == "apiChats" {
|
||||
if let jApiChats = jResp["apiChats"] as? NSDictionary,
|
||||
let user: User = try? decodeObject(jApiChats["user"] as Any),
|
||||
let jChats = jApiChats["chats"] as? NSArray {
|
||||
let chats = jChats.map { jChat in
|
||||
if let chatData = try? parseChatData(jChat) {
|
||||
@@ -141,13 +142,14 @@ public func chatResponse(_ s: String) -> ChatResponse {
|
||||
}
|
||||
return ChatData.invalidJSON(prettyJSON(jChat) ?? "")
|
||||
}
|
||||
return .apiChats(chats: chats)
|
||||
return .apiChats(user: user, chats: chats)
|
||||
}
|
||||
} else if type == "apiChat" {
|
||||
if let jApiChat = jResp["apiChat"] as? NSDictionary,
|
||||
let user: User = try? decodeObject(jApiChat["user"] as Any),
|
||||
let jChat = jApiChat["chat"] as? NSDictionary,
|
||||
let chat = try? parseChatData(jChat) {
|
||||
return .apiChat(chat: chat)
|
||||
return .apiChat(user: user, chat: chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+241
-202
@@ -15,6 +15,9 @@ let jsonEncoder = getJSONEncoder()
|
||||
public enum ChatCommand {
|
||||
case showActiveUser
|
||||
case createActiveUser(profile: Profile)
|
||||
case listUsers
|
||||
case apiSetActiveUser(userId: Int64)
|
||||
case apiDeleteUser(userId: Int64, delSMPQueues: Bool)
|
||||
case startChat(subscribe: Bool, expire: Bool)
|
||||
case apiStopChat
|
||||
case apiActivateChat
|
||||
@@ -25,7 +28,7 @@ public enum ChatCommand {
|
||||
case apiImportArchive(config: ArchiveConfig)
|
||||
case apiDeleteStorage
|
||||
case apiStorageEncryption(config: DBEncryptionConfig)
|
||||
case apiGetChats
|
||||
case apiGetChats(userId: Int64)
|
||||
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String)
|
||||
case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool)
|
||||
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool)
|
||||
@@ -35,7 +38,7 @@ public enum ChatCommand {
|
||||
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
|
||||
case apiDeleteToken(token: DeviceToken)
|
||||
case apiGetNtfMessage(nonce: String, encNtfInfo: String)
|
||||
case newGroup(groupProfile: GroupProfile)
|
||||
case apiNewGroup(userId: Int64, groupProfile: GroupProfile)
|
||||
case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole)
|
||||
case apiJoinGroup(groupId: Int64)
|
||||
case apiMemberRole(groupId: Int64, memberId: Int64, memberRole: GroupMemberRole)
|
||||
@@ -46,11 +49,11 @@ public enum ChatCommand {
|
||||
case apiCreateGroupLink(groupId: Int64)
|
||||
case apiDeleteGroupLink(groupId: Int64)
|
||||
case apiGetGroupLink(groupId: Int64)
|
||||
case getUserSMPServers
|
||||
case setUserSMPServers(smpServers: [ServerCfg])
|
||||
case testSMPServer(smpServer: String)
|
||||
case apiSetChatItemTTL(seconds: Int64?)
|
||||
case apiGetChatItemTTL
|
||||
case apiGetUserSMPServers(userId: Int64)
|
||||
case apiSetUserSMPServers(userId: Int64, smpServers: [ServerCfg])
|
||||
case testSMPServer(userId: Int64, smpServer: String)
|
||||
case apiSetChatItemTTL(userId: Int64, seconds: Int64?)
|
||||
case apiGetChatItemTTL(userId: Int64)
|
||||
case apiSetNetworkConfig(networkConfig: NetCfg)
|
||||
case apiGetNetworkConfig
|
||||
case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
|
||||
@@ -62,19 +65,19 @@ public enum ChatCommand {
|
||||
case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64)
|
||||
case apiVerifyContact(contactId: Int64, connectionCode: String?)
|
||||
case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?)
|
||||
case addContact
|
||||
case connect(connReq: String)
|
||||
case apiAddContact(userId: Int64)
|
||||
case apiConnect(userId: Int64, connReq: String)
|
||||
case apiDeleteChat(type: ChatType, id: Int64)
|
||||
case apiClearChat(type: ChatType, id: Int64)
|
||||
case listContacts
|
||||
case apiUpdateProfile(profile: Profile)
|
||||
case apiListContacts(userId: Int64)
|
||||
case apiUpdateProfile(userId: Int64, profile: Profile)
|
||||
case apiSetContactPrefs(contactId: Int64, preferences: Preferences)
|
||||
case apiSetContactAlias(contactId: Int64, localAlias: String)
|
||||
case apiSetConnectionAlias(connId: Int64, localAlias: String)
|
||||
case createMyAddress
|
||||
case deleteMyAddress
|
||||
case showMyAddress
|
||||
case addressAutoAccept(autoAccept: AutoAccept?)
|
||||
case apiCreateMyAddress(userId: Int64)
|
||||
case apiDeleteMyAddress(userId: Int64)
|
||||
case apiShowMyAddress(userId: Int64)
|
||||
case apiAddressAutoAccept(userId: Int64, autoAccept: AutoAccept?)
|
||||
case apiAcceptContact(contactReqId: Int64)
|
||||
case apiRejectContact(contactReqId: Int64)
|
||||
// WebRTC calls
|
||||
@@ -96,7 +99,10 @@ public enum ChatCommand {
|
||||
get {
|
||||
switch self {
|
||||
case .showActiveUser: return "/u"
|
||||
case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)"
|
||||
case let .createActiveUser(profile): return "/create user \(profile.displayName) \(profile.fullName)"
|
||||
case .listUsers: return "/users"
|
||||
case let .apiSetActiveUser(userId): return "/_user \(userId)"
|
||||
case let .apiDeleteUser(userId, delSMPQueues): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))"
|
||||
case let .startChat(subscribe, expire): return "/_start subscribe=\(onOff(subscribe)) expire=\(onOff(expire))"
|
||||
case .apiStopChat: return "/_stop"
|
||||
case .apiActivateChat: return "/_app activate"
|
||||
@@ -107,7 +113,7 @@ public enum ChatCommand {
|
||||
case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))"
|
||||
case .apiDeleteStorage: return "/_db delete"
|
||||
case let .apiStorageEncryption(cfg): return "/_db encryption \(encodeJSON(cfg))"
|
||||
case .apiGetChats: return "/_get chats pcc=on"
|
||||
case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
|
||||
case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" +
|
||||
(search == "" ? "" : " search=\(search)")
|
||||
case let .apiSendMessage(type, id, file, quotedItemId, mc, live):
|
||||
@@ -120,7 +126,7 @@ public enum ChatCommand {
|
||||
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
|
||||
case let .apiDeleteToken(token): return "/_ntf delete \(token.cmdString)"
|
||||
case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)"
|
||||
case let .newGroup(groupProfile): return "/_group \(encodeJSON(groupProfile))"
|
||||
case let .apiNewGroup(userId, groupProfile): return "/_group \(userId) \(encodeJSON(groupProfile))"
|
||||
case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)"
|
||||
case let .apiJoinGroup(groupId): return "/_join #\(groupId)"
|
||||
case let .apiMemberRole(groupId, memberId, memberRole): return "/_member role #\(groupId) \(memberId) \(memberRole.rawValue)"
|
||||
@@ -131,11 +137,11 @@ public enum ChatCommand {
|
||||
case let .apiCreateGroupLink(groupId): return "/_create link #\(groupId)"
|
||||
case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)"
|
||||
case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)"
|
||||
case .getUserSMPServers: return "/smp"
|
||||
case let .setUserSMPServers(smpServers): return "/_smp \(smpServersStr(smpServers: smpServers))"
|
||||
case let .testSMPServer(smpServer): return "/smp test \(smpServer)"
|
||||
case let .apiSetChatItemTTL(seconds): return "/_ttl \(chatItemTTLStr(seconds: seconds))"
|
||||
case .apiGetChatItemTTL: return "/ttl"
|
||||
case let .apiGetUserSMPServers(userId): return "/_smp \(userId)"
|
||||
case let .apiSetUserSMPServers(userId, smpServers): return "/_smp \(userId) \(smpServersStr(smpServers: smpServers))"
|
||||
case let .testSMPServer(userId, smpServer): return "/smp test \(userId) \(smpServer)"
|
||||
case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))"
|
||||
case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
|
||||
case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
|
||||
case .apiGetNetworkConfig: return "/network"
|
||||
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
|
||||
@@ -149,19 +155,19 @@ public enum ChatCommand {
|
||||
case let .apiVerifyContact(contactId, .none): return "/_verify code @\(contactId)"
|
||||
case let .apiVerifyGroupMember(groupId, groupMemberId, .some(connectionCode)): return "/_verify code #\(groupId) \(groupMemberId) \(connectionCode)"
|
||||
case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)"
|
||||
case .addContact: return "/connect"
|
||||
case let .connect(connReq): return "/connect \(connReq)"
|
||||
case let .apiAddContact(userId): return "/_connect \(userId)"
|
||||
case let .apiConnect(userId, connReq): return "/_connect \(userId) \(connReq)"
|
||||
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
|
||||
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
|
||||
case .listContacts: return "/contacts"
|
||||
case let .apiUpdateProfile(profile): return "/_profile \(encodeJSON(profile))"
|
||||
case let .apiListContacts(userId): return "/_contacts \(userId)"
|
||||
case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))"
|
||||
case let .apiSetContactPrefs(contactId, preferences): return "/_set prefs @\(contactId) \(encodeJSON(preferences))"
|
||||
case let .apiSetContactAlias(contactId, localAlias): return "/_set alias @\(contactId) \(localAlias.trimmingCharacters(in: .whitespaces))"
|
||||
case let .apiSetConnectionAlias(connId, localAlias): return "/_set alias :\(connId) \(localAlias.trimmingCharacters(in: .whitespaces))"
|
||||
case .createMyAddress: return "/address"
|
||||
case .deleteMyAddress: return "/delete_address"
|
||||
case .showMyAddress: return "/show_address"
|
||||
case let .addressAutoAccept(autoAccept): return "/auto_accept \(AutoAccept.cmdString(autoAccept))"
|
||||
case let .apiCreateMyAddress(userId): return "/_address \(userId)"
|
||||
case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)"
|
||||
case let .apiShowMyAddress(userId): return "/_show_address \(userId)"
|
||||
case let .apiAddressAutoAccept(userId, autoAccept): return "/_auto_accept \(userId) \(AutoAccept.cmdString(autoAccept))"
|
||||
case let .apiAcceptContact(contactReqId): return "/_accept \(contactReqId)"
|
||||
case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)"
|
||||
case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))"
|
||||
@@ -186,6 +192,9 @@ public enum ChatCommand {
|
||||
switch self {
|
||||
case .showActiveUser: return "showActiveUser"
|
||||
case .createActiveUser: return "createActiveUser"
|
||||
case .listUsers: return "listUsers"
|
||||
case .apiSetActiveUser: return "apiSetActiveUser"
|
||||
case .apiDeleteUser: return "apiDeleteUser"
|
||||
case .startChat: return "startChat"
|
||||
case .apiStopChat: return "apiStopChat"
|
||||
case .apiActivateChat: return "apiActivateChat"
|
||||
@@ -206,7 +215,7 @@ public enum ChatCommand {
|
||||
case .apiVerifyToken: return "apiVerifyToken"
|
||||
case .apiDeleteToken: return "apiDeleteToken"
|
||||
case .apiGetNtfMessage: return "apiGetNtfMessage"
|
||||
case .newGroup: return "newGroup"
|
||||
case .apiNewGroup: return "apiNewGroup"
|
||||
case .apiAddMember: return "apiAddMember"
|
||||
case .apiJoinGroup: return "apiJoinGroup"
|
||||
case .apiMemberRole: return "apiMemberRole"
|
||||
@@ -217,8 +226,8 @@ public enum ChatCommand {
|
||||
case .apiCreateGroupLink: return "apiCreateGroupLink"
|
||||
case .apiDeleteGroupLink: return "apiDeleteGroupLink"
|
||||
case .apiGetGroupLink: return "apiGetGroupLink"
|
||||
case .getUserSMPServers: return "getUserSMPServers"
|
||||
case .setUserSMPServers: return "setUserSMPServers"
|
||||
case .apiGetUserSMPServers: return "apiGetUserSMPServers"
|
||||
case .apiSetUserSMPServers: return "apiSetUserSMPServers"
|
||||
case .testSMPServer: return "testSMPServer"
|
||||
case .apiSetChatItemTTL: return "apiSetChatItemTTL"
|
||||
case .apiGetChatItemTTL: return "apiGetChatItemTTL"
|
||||
@@ -233,19 +242,19 @@ public enum ChatCommand {
|
||||
case .apiGetGroupMemberCode: return "apiGetGroupMemberCode"
|
||||
case .apiVerifyContact: return "apiVerifyContact"
|
||||
case .apiVerifyGroupMember: return "apiVerifyGroupMember"
|
||||
case .addContact: return "addContact"
|
||||
case .connect: return "connect"
|
||||
case .apiAddContact: return "apiAddContact"
|
||||
case .apiConnect: return "apiConnect"
|
||||
case .apiDeleteChat: return "apiDeleteChat"
|
||||
case .apiClearChat: return "apiClearChat"
|
||||
case .listContacts: return "listContacts"
|
||||
case .apiListContacts: return "apiListContacts"
|
||||
case .apiUpdateProfile: return "apiUpdateProfile"
|
||||
case .apiSetContactPrefs: return "apiSetContactPrefs"
|
||||
case .apiSetContactAlias: return "apiSetContactAlias"
|
||||
case .apiSetConnectionAlias: return "apiSetConnectionAlias"
|
||||
case .createMyAddress: return "createMyAddress"
|
||||
case .deleteMyAddress: return "deleteMyAddress"
|
||||
case .showMyAddress: return "showMyAddress"
|
||||
case .addressAutoAccept: return "addressAutoAccept"
|
||||
case .apiCreateMyAddress: return "apiCreateMyAddress"
|
||||
case .apiDeleteMyAddress: return "apiDeleteMyAddress"
|
||||
case .apiShowMyAddress: return "apiShowMyAddress"
|
||||
case .apiAddressAutoAccept: return "apiAddressAutoAccept"
|
||||
case .apiAcceptContact: return "apiAcceptContact"
|
||||
case .apiRejectContact: return "apiRejectContact"
|
||||
case .apiSendCallInvitation: return "apiSendCallInvitation"
|
||||
@@ -305,113 +314,115 @@ struct APIResponse: Decodable {
|
||||
public enum ChatResponse: Decodable, Error {
|
||||
case response(type: String, json: String)
|
||||
case activeUser(user: User)
|
||||
case usersList(users: [UserInfo])
|
||||
case chatStarted
|
||||
case chatRunning
|
||||
case chatStopped
|
||||
case chatSuspended
|
||||
case apiChats(chats: [ChatData])
|
||||
case apiChat(chat: ChatData)
|
||||
case userSMPServers(smpServers: [ServerCfg], presetSMPServers: [String])
|
||||
case smpTestResult(smpTestFailure: SMPTestFailure?)
|
||||
case chatItemTTL(chatItemTTL: Int64?)
|
||||
case apiChats(user: User, chats: [ChatData])
|
||||
case apiChat(user: User, chat: ChatData)
|
||||
case userSMPServers(user: User, smpServers: [ServerCfg], presetSMPServers: [String])
|
||||
case smpTestResult(user: User, smpTestFailure: SMPTestFailure?)
|
||||
case chatItemTTL(user: User, chatItemTTL: Int64?)
|
||||
case networkConfig(networkConfig: NetCfg)
|
||||
case contactInfo(contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?)
|
||||
case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?)
|
||||
case contactCode(contact: Contact, connectionCode: String)
|
||||
case groupMemberCode(groupInfo: GroupInfo, member: GroupMember, connectionCode: String)
|
||||
case connectionVerified(verified: Bool, expectedCode: String)
|
||||
case invitation(connReqInvitation: String)
|
||||
case sentConfirmation
|
||||
case sentInvitation
|
||||
case contactAlreadyExists(contact: Contact)
|
||||
case contactDeleted(contact: Contact)
|
||||
case chatCleared(chatInfo: ChatInfo)
|
||||
case userProfileNoChange
|
||||
case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
|
||||
case contactAliasUpdated(toContact: Contact)
|
||||
case connectionAliasUpdated(toConnection: PendingContactConnection)
|
||||
case contactPrefsUpdated(fromContact: Contact, toContact: Contact)
|
||||
case userContactLink(contactLink: UserContactLink)
|
||||
case userContactLinkUpdated(contactLink: UserContactLink)
|
||||
case userContactLinkCreated(connReqContact: String)
|
||||
case userContactLinkDeleted
|
||||
case contactConnected(contact: Contact, userCustomProfile: Profile?)
|
||||
case contactConnecting(contact: Contact)
|
||||
case receivedContactRequest(contactRequest: UserContactRequest)
|
||||
case acceptingContactRequest(contact: Contact)
|
||||
case contactRequestRejected
|
||||
case contactUpdated(toContact: Contact)
|
||||
case contactInfo(user: User, contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?)
|
||||
case groupMemberInfo(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?)
|
||||
case contactCode(user: User, contact: Contact, connectionCode: String)
|
||||
case groupMemberCode(user: User, groupInfo: GroupInfo, member: GroupMember, connectionCode: String)
|
||||
case connectionVerified(user: User, verified: Bool, expectedCode: String)
|
||||
case invitation(user: User, connReqInvitation: String)
|
||||
case sentConfirmation(user: User)
|
||||
case sentInvitation(user: User)
|
||||
case contactAlreadyExists(user: User, contact: Contact)
|
||||
case contactDeleted(user: User, contact: Contact)
|
||||
case chatCleared(user: User, chatInfo: ChatInfo)
|
||||
case userProfileNoChange(user: User)
|
||||
case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile)
|
||||
case contactAliasUpdated(user: User, toContact: Contact)
|
||||
case connectionAliasUpdated(user: User, toConnection: PendingContactConnection)
|
||||
case contactPrefsUpdated(user: User, fromContact: Contact, toContact: Contact)
|
||||
case userContactLink(user: User, contactLink: UserContactLink)
|
||||
case userContactLinkUpdated(user: User, contactLink: UserContactLink)
|
||||
case userContactLinkCreated(user: User, connReqContact: String)
|
||||
case userContactLinkDeleted(user: User)
|
||||
case contactConnected(user: User, contact: Contact, userCustomProfile: Profile?)
|
||||
case contactConnecting(user: User, contact: Contact)
|
||||
case receivedContactRequest(user: User, contactRequest: UserContactRequest)
|
||||
case acceptingContactRequest(user: User, contact: Contact)
|
||||
case contactRequestRejected(user: User)
|
||||
case contactUpdated(user: User, toContact: Contact)
|
||||
case contactsSubscribed(server: String, contactRefs: [ContactRef])
|
||||
case contactsDisconnected(server: String, contactRefs: [ContactRef])
|
||||
case contactSubError(contact: Contact, chatError: ChatError)
|
||||
case contactSubSummary(contactSubscriptions: [ContactSubStatus])
|
||||
case groupSubscribed(groupInfo: GroupInfo)
|
||||
case memberSubErrors(memberSubErrors: [MemberSubError])
|
||||
case groupEmpty(groupInfo: GroupInfo)
|
||||
case contactSubError(user: User, contact: Contact, chatError: ChatError)
|
||||
case contactSubSummary(user: User, contactSubscriptions: [ContactSubStatus])
|
||||
case groupSubscribed(user: User, groupInfo: GroupInfo)
|
||||
case memberSubErrors(user: User, memberSubErrors: [MemberSubError])
|
||||
case groupEmpty(user: User, groupInfo: GroupInfo)
|
||||
case userContactLinkSubscribed
|
||||
case newChatItem(chatItem: AChatItem)
|
||||
case chatItemStatusUpdated(chatItem: AChatItem)
|
||||
case chatItemUpdated(chatItem: AChatItem)
|
||||
case chatItemDeleted(deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool)
|
||||
case contactsList(contacts: [Contact])
|
||||
case newChatItem(user: User, chatItem: AChatItem)
|
||||
case chatItemStatusUpdated(user: User, chatItem: AChatItem)
|
||||
case chatItemUpdated(user: User, chatItem: AChatItem)
|
||||
case chatItemDeleted(user: User, deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool)
|
||||
case contactsList(user: User, contacts: [Contact])
|
||||
// group events
|
||||
case groupCreated(groupInfo: GroupInfo)
|
||||
case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact, member: GroupMember)
|
||||
case userAcceptedGroupSent(groupInfo: GroupInfo, hostContact: Contact?)
|
||||
case userDeletedMember(groupInfo: GroupInfo, member: GroupMember)
|
||||
case leftMemberUser(groupInfo: GroupInfo)
|
||||
case groupMembers(group: Group)
|
||||
case receivedGroupInvitation(groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole)
|
||||
case groupDeletedUser(groupInfo: GroupInfo)
|
||||
case joinedGroupMemberConnecting(groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember)
|
||||
case memberRole(groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole)
|
||||
case memberRoleUser(groupInfo: GroupInfo, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole)
|
||||
case deletedMemberUser(groupInfo: GroupInfo, member: GroupMember)
|
||||
case deletedMember(groupInfo: GroupInfo, byMember: GroupMember, deletedMember: GroupMember)
|
||||
case leftMember(groupInfo: GroupInfo, member: GroupMember)
|
||||
case groupDeleted(groupInfo: GroupInfo, member: GroupMember)
|
||||
case contactsMerged(intoContact: Contact, mergedContact: Contact)
|
||||
case groupInvitation(groupInfo: GroupInfo) // unused
|
||||
case userJoinedGroup(groupInfo: GroupInfo)
|
||||
case joinedGroupMember(groupInfo: GroupInfo, member: GroupMember)
|
||||
case connectedToGroupMember(groupInfo: GroupInfo, member: GroupMember)
|
||||
case groupRemoved(groupInfo: GroupInfo) // unused
|
||||
case groupUpdated(toGroup: GroupInfo)
|
||||
case groupLinkCreated(groupInfo: GroupInfo, connReqContact: String)
|
||||
case groupLink(groupInfo: GroupInfo, connReqContact: String)
|
||||
case groupLinkDeleted(groupInfo: GroupInfo)
|
||||
case groupCreated(user: User, groupInfo: GroupInfo)
|
||||
case sentGroupInvitation(user: User, groupInfo: GroupInfo, contact: Contact, member: GroupMember)
|
||||
case userAcceptedGroupSent(user: User, groupInfo: GroupInfo, hostContact: Contact?)
|
||||
case userDeletedMember(user: User, groupInfo: GroupInfo, member: GroupMember)
|
||||
case leftMemberUser(user: User, groupInfo: GroupInfo)
|
||||
case groupMembers(user: User, group: Group)
|
||||
case receivedGroupInvitation(user: User, groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole)
|
||||
case groupDeletedUser(user: User, groupInfo: GroupInfo)
|
||||
case joinedGroupMemberConnecting(user: User, groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember)
|
||||
case memberRole(user: User, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole)
|
||||
case memberRoleUser(user: User, groupInfo: GroupInfo, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole)
|
||||
case deletedMemberUser(user: User, groupInfo: GroupInfo, member: GroupMember)
|
||||
case deletedMember(user: User, groupInfo: GroupInfo, byMember: GroupMember, deletedMember: GroupMember)
|
||||
case leftMember(user: User, groupInfo: GroupInfo, member: GroupMember)
|
||||
case groupDeleted(user: User, groupInfo: GroupInfo, member: GroupMember)
|
||||
case contactsMerged(user: User, intoContact: Contact, mergedContact: Contact)
|
||||
case groupInvitation(user: User, groupInfo: GroupInfo) // unused
|
||||
case userJoinedGroup(user: User, groupInfo: GroupInfo)
|
||||
case joinedGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember)
|
||||
case connectedToGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember)
|
||||
case groupRemoved(user: User, groupInfo: GroupInfo) // unused
|
||||
case groupUpdated(user: User, toGroup: GroupInfo)
|
||||
case groupLinkCreated(user: User, groupInfo: GroupInfo, connReqContact: String)
|
||||
case groupLink(user: User, groupInfo: GroupInfo, connReqContact: String)
|
||||
case groupLinkDeleted(user: User, groupInfo: GroupInfo)
|
||||
// receiving file events
|
||||
case rcvFileAccepted(chatItem: AChatItem)
|
||||
case rcvFileAcceptedSndCancelled(rcvFileTransfer: RcvFileTransfer)
|
||||
case rcvFileStart(chatItem: AChatItem)
|
||||
case rcvFileComplete(chatItem: AChatItem)
|
||||
case rcvFileAccepted(user: User, chatItem: AChatItem)
|
||||
case rcvFileAcceptedSndCancelled(user: User, rcvFileTransfer: RcvFileTransfer)
|
||||
case rcvFileStart(user: User, chatItem: AChatItem)
|
||||
case rcvFileComplete(user: User, chatItem: AChatItem)
|
||||
// sending file events
|
||||
case sndFileStart(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndFileComplete(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndFileStart(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndFileComplete(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndFileCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndFileRcvCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndGroupFileCancelled(chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
|
||||
case sndFileRcvCancelled(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndGroupFileCancelled(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
|
||||
case callInvitation(callInvitation: RcvCallInvitation)
|
||||
case callOffer(contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
|
||||
case callAnswer(contact: Contact, answer: WebRTCSession)
|
||||
case callExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo)
|
||||
case callEnded(contact: Contact)
|
||||
case callOffer(user: User, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
|
||||
case callAnswer(user: User, contact: Contact, answer: WebRTCSession)
|
||||
case callExtraInfo(user: User, contact: Contact, extraInfo: WebRTCExtraInfo)
|
||||
case callEnded(user: User, contact: Contact)
|
||||
case callInvitations(callInvitations: [RcvCallInvitation])
|
||||
case ntfTokenStatus(status: NtfTknStatus)
|
||||
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
|
||||
case ntfMessages(connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
||||
case newContactConnection(connection: PendingContactConnection)
|
||||
case contactConnectionDeleted(connection: PendingContactConnection)
|
||||
case ntfMessages(user_: User?, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
||||
case newContactConnection(user: User, connection: PendingContactConnection)
|
||||
case contactConnectionDeleted(user: User, connection: PendingContactConnection)
|
||||
case versionInfo(versionInfo: CoreVersionInfo)
|
||||
case cmdOk
|
||||
case chatCmdError(chatError: ChatError)
|
||||
case chatError(chatError: ChatError)
|
||||
case cmdOk(user: User?)
|
||||
case chatCmdError(user: User?, chatError: ChatError)
|
||||
case chatError(user: User?, chatError: ChatError)
|
||||
|
||||
public var responseType: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .response(type, _): return "* \(type)"
|
||||
case .activeUser: return "activeUser"
|
||||
case .usersList: return "usersList"
|
||||
case .chatStarted: return "chatStarted"
|
||||
case .chatRunning: return "chatRunning"
|
||||
case .chatStopped: return "chatStopped"
|
||||
@@ -519,109 +530,117 @@ public enum ChatResponse: Decodable, Error {
|
||||
switch self {
|
||||
case let .response(_, json): return json
|
||||
case let .activeUser(user): return String(describing: user)
|
||||
case let .usersList(users): return String(describing: users)
|
||||
case .chatStarted: return noDetails
|
||||
case .chatRunning: return noDetails
|
||||
case .chatStopped: return noDetails
|
||||
case .chatSuspended: return noDetails
|
||||
case let .apiChats(chats): return String(describing: chats)
|
||||
case let .apiChat(chat): return String(describing: chat)
|
||||
case let .userSMPServers(smpServers, presetServers): return "smpServers: \(String(describing: smpServers))\npresetServers: \(String(describing: presetServers))"
|
||||
case let .smpTestResult(smpTestFailure): return String(describing: smpTestFailure)
|
||||
case let .chatItemTTL(chatItemTTL): return String(describing: chatItemTTL)
|
||||
case let .apiChats(u, chats): return withUser(u, String(describing: chats))
|
||||
case let .apiChat(u, chat): return withUser(u, String(describing: chat))
|
||||
case let .userSMPServers(u, smpServers, presetServers): return withUser(u, "smpServers: \(String(describing: smpServers))\npresetServers: \(String(describing: presetServers))")
|
||||
case let .smpTestResult(u, smpTestFailure): return withUser(u, String(describing: smpTestFailure))
|
||||
case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL))
|
||||
case let .networkConfig(networkConfig): return String(describing: networkConfig)
|
||||
case let .contactInfo(contact, connectionStats, customUserProfile): return "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))"
|
||||
case let .groupMemberInfo(groupInfo, member, connectionStats_): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_)))"
|
||||
case let .contactCode(contact, connectionCode): return "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)"
|
||||
case let .groupMemberCode(groupInfo, member, connectionCode): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)"
|
||||
case let .connectionVerified(verified, expectedCode): return "verified: \(verified)\nconnectionCode: \(expectedCode)"
|
||||
case let .invitation(connReqInvitation): return connReqInvitation
|
||||
case let .contactInfo(u, contact, connectionStats, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))")
|
||||
case let .groupMemberInfo(u, groupInfo, member, connectionStats_): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_)))")
|
||||
case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)")
|
||||
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
|
||||
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
|
||||
case let .invitation(u, connReqInvitation): return withUser(u, connReqInvitation)
|
||||
case .sentConfirmation: return noDetails
|
||||
case .sentInvitation: return noDetails
|
||||
case let .contactAlreadyExists(contact): return String(describing: contact)
|
||||
case let .contactDeleted(contact): return String(describing: contact)
|
||||
case let .chatCleared(chatInfo): return String(describing: chatInfo)
|
||||
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
|
||||
case let .contactDeleted(u, contact): return withUser(u, String(describing: contact))
|
||||
case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo))
|
||||
case .userProfileNoChange: return noDetails
|
||||
case let .userProfileUpdated(_, toProfile): return String(describing: toProfile)
|
||||
case let .contactAliasUpdated(toContact): return String(describing: toContact)
|
||||
case let .connectionAliasUpdated(toConnection): return String(describing: toConnection)
|
||||
case let .contactPrefsUpdated(fromContact, toContact): return "fromContact: \(String(describing: fromContact))\ntoContact: \(String(describing: toContact))"
|
||||
case let .userContactLink(contactLink): return contactLink.responseDetails
|
||||
case let .userContactLinkUpdated(contactLink): return contactLink.responseDetails
|
||||
case let .userContactLinkCreated(connReq): return connReq
|
||||
case let .userProfileUpdated(u, _, toProfile): return withUser(u, String(describing: toProfile))
|
||||
case let .contactAliasUpdated(u, toContact): return withUser(u, String(describing: toContact))
|
||||
case let .connectionAliasUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
|
||||
case let .contactPrefsUpdated(u, fromContact, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\ntoContact: \(String(describing: toContact))")
|
||||
case let .userContactLink(u, contactLink): return withUser(u, contactLink.responseDetails)
|
||||
case let .userContactLinkUpdated(u, contactLink): return withUser(u, contactLink.responseDetails)
|
||||
case let .userContactLinkCreated(u, connReq): return withUser(u, connReq)
|
||||
case .userContactLinkDeleted: return noDetails
|
||||
case let .contactConnected(contact, _): return String(describing: contact)
|
||||
case let .contactConnecting(contact): return String(describing: contact)
|
||||
case let .receivedContactRequest(contactRequest): return String(describing: contactRequest)
|
||||
case let .acceptingContactRequest(contact): return String(describing: contact)
|
||||
case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact))
|
||||
case let .contactConnecting(u, contact): return withUser(u, String(describing: contact))
|
||||
case let .receivedContactRequest(u, contactRequest): return withUser(u, String(describing: contactRequest))
|
||||
case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact))
|
||||
case .contactRequestRejected: return noDetails
|
||||
case let .contactUpdated(toContact): return String(describing: toContact)
|
||||
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
|
||||
case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
|
||||
case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
|
||||
case let .contactSubError(contact, chatError): return "contact:\n\(String(describing: contact))\nerror:\n\(String(describing: chatError))"
|
||||
case let .contactSubSummary(contactSubscriptions): return String(describing: contactSubscriptions)
|
||||
case let .groupSubscribed(groupInfo): return String(describing: groupInfo)
|
||||
case let .memberSubErrors(memberSubErrors): return String(describing: memberSubErrors)
|
||||
case let .groupEmpty(groupInfo): return String(describing: groupInfo)
|
||||
case let .contactSubError(u, contact, chatError): return withUser(u, "contact:\n\(String(describing: contact))\nerror:\n\(String(describing: chatError))")
|
||||
case let .contactSubSummary(u, contactSubscriptions): return withUser(u, String(describing: contactSubscriptions))
|
||||
case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .memberSubErrors(u, memberSubErrors): return withUser(u, String(describing: memberSubErrors))
|
||||
case let .groupEmpty(u, groupInfo): return withUser(u, 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(deletedChatItem, toChatItem, byUser): return "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))\nbyUser: \(byUser)"
|
||||
case let .contactsList(contacts): return String(describing: contacts)
|
||||
case let .groupCreated(groupInfo): return String(describing: groupInfo)
|
||||
case let .sentGroupInvitation(groupInfo, contact, member): return "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)"
|
||||
case let .userAcceptedGroupSent(groupInfo, hostContact): return "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))"
|
||||
case let .userDeletedMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
|
||||
case let .leftMemberUser(groupInfo): return String(describing: groupInfo)
|
||||
case let .groupMembers(group): return String(describing: group)
|
||||
case let .receivedGroupInvitation(groupInfo, contact, memberRole): return "groupInfo: \(groupInfo)\ncontact: \(contact)\nmemberRole: \(memberRole)"
|
||||
case let .groupDeletedUser(groupInfo): return String(describing: groupInfo)
|
||||
case let .joinedGroupMemberConnecting(groupInfo, hostMember, member): return "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\nmember: \(member)"
|
||||
case let .memberRole(groupInfo, byMember, member, fromRole, toRole): return "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)"
|
||||
case let .memberRoleUser(groupInfo, member, fromRole, toRole): return "groupInfo: \(groupInfo)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)"
|
||||
case let .deletedMemberUser(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
|
||||
case let .deletedMember(groupInfo, byMember, deletedMember): return "groupInfo: \(groupInfo)\nbyMember: \(byMember)\ndeletedMember: \(deletedMember)"
|
||||
case let .leftMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
|
||||
case let .groupDeleted(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
|
||||
case let .contactsMerged(intoContact, mergedContact): return "intoContact: \(intoContact)\nmergedContact: \(mergedContact)"
|
||||
case let .groupInvitation(groupInfo): return String(describing: groupInfo)
|
||||
case let .userJoinedGroup(groupInfo): return String(describing: groupInfo)
|
||||
case let .joinedGroupMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
|
||||
case let .connectedToGroupMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
|
||||
case let .groupRemoved(groupInfo): return String(describing: groupInfo)
|
||||
case let .groupUpdated(toGroup): return String(describing: toGroup)
|
||||
case let .groupLinkCreated(groupInfo, connReqContact): return "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)"
|
||||
case let .groupLink(groupInfo, connReqContact): return "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)"
|
||||
case let .groupLinkDeleted(groupInfo): return String(describing: groupInfo)
|
||||
case let .rcvFileAccepted(chatItem): return String(describing: chatItem)
|
||||
case let .newChatItem(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .chatItemStatusUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .chatItemDeleted(u, deletedChatItem, toChatItem, byUser): return withUser(u, "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))\nbyUser: \(byUser)")
|
||||
case let .contactsList(u, contacts): return withUser(u, String(describing: contacts))
|
||||
case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .sentGroupInvitation(u, groupInfo, contact, member): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)")
|
||||
case let .userAcceptedGroupSent(u, groupInfo, hostContact): return withUser(u, "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))")
|
||||
case let .userDeletedMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .groupMembers(u, group): return withUser(u, String(describing: group))
|
||||
case let .receivedGroupInvitation(u, groupInfo, contact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmemberRole: \(memberRole)")
|
||||
case let .groupDeletedUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .joinedGroupMemberConnecting(u, groupInfo, hostMember, member): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\nmember: \(member)")
|
||||
case let .memberRole(u, groupInfo, byMember, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)")
|
||||
case let .memberRoleUser(u, groupInfo, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)")
|
||||
case let .deletedMemberUser(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .deletedMember(u, groupInfo, byMember, deletedMember): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\ndeletedMember: \(deletedMember)")
|
||||
case let .leftMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .groupDeleted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .contactsMerged(u, intoContact, mergedContact): return withUser(u, "intoContact: \(intoContact)\nmergedContact: \(mergedContact)")
|
||||
case let .groupInvitation(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .userJoinedGroup(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .joinedGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .connectedToGroupMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .groupRemoved(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
|
||||
case let .groupLinkCreated(u, groupInfo, connReqContact): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)")
|
||||
case let .groupLink(u, groupInfo, connReqContact): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)")
|
||||
case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case .rcvFileAcceptedSndCancelled: return noDetails
|
||||
case let .rcvFileStart(chatItem): return String(describing: chatItem)
|
||||
case let .rcvFileComplete(chatItem): return String(describing: chatItem)
|
||||
case let .sndFileStart(chatItem, _): return String(describing: chatItem)
|
||||
case let .sndFileComplete(chatItem, _): return String(describing: chatItem)
|
||||
case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileCancelled(chatItem, _): return String(describing: chatItem)
|
||||
case let .sndFileRcvCancelled(chatItem, _): return String(describing: chatItem)
|
||||
case let .sndGroupFileCancelled(chatItem, _, _): return String(describing: chatItem)
|
||||
case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndGroupFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
|
||||
case let .callInvitation(inv): return String(describing: inv)
|
||||
case let .callOffer(contact, callType, offer, sharedKey, askConfirmation): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))"
|
||||
case let .callAnswer(contact, answer): return "contact: \(contact.id)\nanswer: \(String(describing: answer))"
|
||||
case let .callExtraInfo(contact, extraInfo): return "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))"
|
||||
case let .callEnded(contact): return "contact: \(contact.id)"
|
||||
case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))")
|
||||
case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))")
|
||||
case let .callExtraInfo(u, contact, extraInfo): return withUser(u, "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))")
|
||||
case let .callEnded(u, contact): return withUser(u, "contact: \(contact.id)")
|
||||
case let .callInvitations(invs): return String(describing: invs)
|
||||
case let .ntfTokenStatus(status): return String(describing: status)
|
||||
case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
|
||||
case let .ntfMessages(connEntity, msgTs, ntfMessages): return "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))"
|
||||
case let .newContactConnection(connection): return String(describing: connection)
|
||||
case let .contactConnectionDeleted(connection): return String(describing: connection)
|
||||
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
|
||||
case let .newContactConnection(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .versionInfo(versionInfo): return String(describing: versionInfo)
|
||||
case .cmdOk: return noDetails
|
||||
case let .chatCmdError(chatError): return String(describing: chatError)
|
||||
case let .chatError(chatError): return String(describing: chatError)
|
||||
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
case let .chatError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var noDetails: String { get { "\(responseType): no details" } }
|
||||
|
||||
private func withUser(_ u: User?, _ s: String) -> String {
|
||||
if let id = u?.userId {
|
||||
return "userId: \(id)\n\(s)"
|
||||
}
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatPagination {
|
||||
@@ -811,6 +830,7 @@ public struct NetCfg: Codable, Equatable {
|
||||
public var socksProxy: String? = nil
|
||||
public var hostMode: HostMode = .publicHost
|
||||
public var requiredHostMode = true
|
||||
public var sessionMode: TransportSessionMode
|
||||
public var tcpConnectTimeout: Int // microseconds
|
||||
public var tcpTimeout: Int // microseconds
|
||||
public var tcpKeepAlive: KeepAliveOpts?
|
||||
@@ -820,6 +840,7 @@ public struct NetCfg: Codable, Equatable {
|
||||
|
||||
public static let defaults: NetCfg = NetCfg(
|
||||
socksProxy: nil,
|
||||
sessionMode: TransportSessionMode.user,
|
||||
tcpConnectTimeout: 10_000_000,
|
||||
tcpTimeout: 7_000_000,
|
||||
tcpKeepAlive: KeepAliveOpts.defaults,
|
||||
@@ -830,6 +851,7 @@ public struct NetCfg: Codable, Equatable {
|
||||
|
||||
public static let proxyDefaults: NetCfg = NetCfg(
|
||||
socksProxy: nil,
|
||||
sessionMode: TransportSessionMode.user,
|
||||
tcpConnectTimeout: 20_000_000,
|
||||
tcpTimeout: 15_000_000,
|
||||
tcpKeepAlive: KeepAliveOpts.defaults,
|
||||
@@ -881,6 +903,22 @@ public enum OnionHosts: String, Identifiable {
|
||||
public static let values: [OnionHosts] = [.no, .prefer, .require]
|
||||
}
|
||||
|
||||
public enum TransportSessionMode: String, Codable, Identifiable {
|
||||
case user
|
||||
case entity
|
||||
|
||||
public var text: LocalizedStringKey {
|
||||
switch self {
|
||||
case .user: return "User profile"
|
||||
case .entity: return "Connection"
|
||||
}
|
||||
}
|
||||
|
||||
public var id: TransportSessionMode { self }
|
||||
|
||||
public static let values: [TransportSessionMode] = [.user, .entity]
|
||||
}
|
||||
|
||||
public struct KeepAliveOpts: Codable, Equatable {
|
||||
public var keepIdle: Int // seconds
|
||||
public var keepIntvl: Int // seconds
|
||||
@@ -1040,6 +1078,7 @@ public enum ChatError: Decodable {
|
||||
public enum ChatErrorType: Decodable {
|
||||
case noActiveUser
|
||||
case activeUserExists
|
||||
case differentActiveUser
|
||||
case chatNotStarted
|
||||
case invalidConnReq
|
||||
case invalidChatMessage(message: String)
|
||||
|
||||
@@ -17,6 +17,7 @@ let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
||||
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline"
|
||||
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
|
||||
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
|
||||
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout"
|
||||
let GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL = "networkSMPPingInterval"
|
||||
@@ -36,6 +37,7 @@ public let groupDefaults = UserDefaults(suiteName: APP_GROUP_NAME)!
|
||||
public func registerGroupDefaults() {
|
||||
groupDefaults.register(defaults: [
|
||||
GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS: OnionHosts.no.rawValue,
|
||||
GROUP_DEFAULT_NETWORK_SESSION_MODE: TransportSessionMode.user.rawValue,
|
||||
GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout,
|
||||
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout,
|
||||
GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL: NetCfg.defaults.smpPingInterval,
|
||||
@@ -107,6 +109,12 @@ public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>(
|
||||
withDefault: .no
|
||||
)
|
||||
|
||||
public let networkSessionModeGroupDefault = EnumDefault<TransportSessionMode>(
|
||||
defaults: groupDefaults,
|
||||
forKey: GROUP_DEFAULT_NETWORK_SESSION_MODE,
|
||||
withDefault: .user
|
||||
)
|
||||
|
||||
public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_STORE_DB_PASSPHRASE)
|
||||
|
||||
public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE)
|
||||
@@ -186,6 +194,7 @@ public class Default<T> {
|
||||
public func getNetCfg() -> NetCfg {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (hostMode, requiredHostMode) = onionHosts.hostMode
|
||||
let sessionMode = networkSessionModeGroupDefault.get()
|
||||
let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
|
||||
let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
|
||||
let smpPingInterval = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL)
|
||||
@@ -203,6 +212,7 @@ public func getNetCfg() -> NetCfg {
|
||||
return NetCfg(
|
||||
hostMode: hostMode,
|
||||
requiredHostMode: requiredHostMode,
|
||||
sessionMode: sessionMode,
|
||||
tcpConnectTimeout: tcpConnectTimeout,
|
||||
tcpTimeout: tcpTimeout,
|
||||
tcpKeepAlive: tcpKeepAlive,
|
||||
@@ -214,6 +224,7 @@ public func getNetCfg() -> NetCfg {
|
||||
|
||||
public func setNetCfg(_ cfg: NetCfg) {
|
||||
networkUseOnionHostsGroupDefault.set(OnionHosts(netCfg: cfg))
|
||||
networkSessionModeGroupDefault.set(cfg.sessionMode)
|
||||
groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
|
||||
groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
|
||||
groupDefaults.set(cfg.smpPingInterval, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL)
|
||||
|
||||
@@ -38,6 +38,7 @@ public struct WebRTCExtraInfo: Codable {
|
||||
}
|
||||
|
||||
public struct RcvCallInvitation: Decodable {
|
||||
public var user: User
|
||||
public var contact: Contact
|
||||
public var callkitUUID: UUID? = UUID()
|
||||
public var callType: CallType
|
||||
@@ -53,6 +54,7 @@ public struct RcvCallInvitation: Decodable {
|
||||
}
|
||||
|
||||
public static let sampleData = RcvCallInvitation(
|
||||
user: User.sampleData,
|
||||
contact: Contact.sampleData,
|
||||
callType: CallType(media: .audio, capabilities: CallCapabilities(encryption: false)),
|
||||
callTs: .now
|
||||
|
||||
@@ -9,19 +9,21 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public struct User: Decodable, NamedChat {
|
||||
var userId: Int64
|
||||
public struct User: Decodable, NamedChat, Identifiable {
|
||||
public var userId: Int64
|
||||
var userContactId: Int64
|
||||
var localDisplayName: ContactName
|
||||
public var profile: LocalProfile
|
||||
public var fullPreferences: FullPreferences
|
||||
var activeUser: Bool
|
||||
public var activeUser: Bool
|
||||
|
||||
public var displayName: String { get { profile.displayName } }
|
||||
public var fullName: String { get { profile.fullName } }
|
||||
public var image: String? { get { profile.image } }
|
||||
public var localAlias: String { get { "" } }
|
||||
|
||||
public var id: Int64 { userId }
|
||||
|
||||
public static let sampleData = User(
|
||||
userId: 1,
|
||||
userContactId: 1,
|
||||
@@ -32,6 +34,23 @@ public struct User: Decodable, NamedChat {
|
||||
)
|
||||
}
|
||||
|
||||
public struct UserInfo: Decodable, Identifiable {
|
||||
public var user: User
|
||||
public var unreadCount: Int
|
||||
|
||||
public init(user: User, unreadCount: Int) {
|
||||
self.user = user
|
||||
self.unreadCount = unreadCount
|
||||
}
|
||||
|
||||
public var id: Int64 { user.userId }
|
||||
|
||||
public static let sampleData = UserInfo(
|
||||
user: User.sampleData,
|
||||
unreadCount: 1
|
||||
)
|
||||
}
|
||||
|
||||
public typealias ContactName = String
|
||||
|
||||
public typealias GroupName = String
|
||||
@@ -1137,6 +1156,8 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
|
||||
public struct ContactRef: Decodable, Equatable {
|
||||
var contactId: Int64
|
||||
public var agentConnId: String
|
||||
var connId: Int64
|
||||
var localDisplayName: ContactName
|
||||
|
||||
public var id: ChatId { get { "@\(contactId)" } }
|
||||
@@ -1148,7 +1169,8 @@ public struct ContactSubStatus: Decodable {
|
||||
}
|
||||
|
||||
public struct Connection: Decodable {
|
||||
var connId: Int64
|
||||
public var connId: Int64
|
||||
public var agentConnId: String
|
||||
var connStatus: ConnStatus
|
||||
public var connLevel: Int
|
||||
public var viaGroupLink: Bool
|
||||
@@ -1159,6 +1181,7 @@ public struct Connection: Decodable {
|
||||
|
||||
static let sampleData = Connection(
|
||||
connId: 1,
|
||||
agentConnId: "abc",
|
||||
connStatus: .ready,
|
||||
connLevel: 0,
|
||||
viaGroupLink: false
|
||||
|
||||
@@ -21,7 +21,7 @@ public let appNotificationId = "chat.simplex.app.notification"
|
||||
|
||||
let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification")
|
||||
|
||||
public func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNMutableNotificationContent {
|
||||
public func createContactRequestNtf(_ user: User, _ contactRequest: UserContactRequest) -> UNMutableNotificationContent {
|
||||
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
|
||||
return createNotification(
|
||||
categoryIdentifier: ntfCategoryContactRequest,
|
||||
@@ -34,11 +34,11 @@ public func createContactRequestNtf(_ contactRequest: UserContactRequest) -> UNM
|
||||
hideContent ? NSLocalizedString("this contact", comment: "notification title") : contactRequest.chatViewName
|
||||
),
|
||||
targetContentIdentifier: nil,
|
||||
userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId]
|
||||
userInfo: ["chatId": contactRequest.id, "contactRequestId": contactRequest.apiId, "userId": user.userId]
|
||||
)
|
||||
}
|
||||
|
||||
public func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificationContent {
|
||||
public func createContactConnectedNtf(_ user: User, _ contact: Contact) -> UNMutableNotificationContent {
|
||||
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
|
||||
return createNotification(
|
||||
categoryIdentifier: ntfCategoryContactConnected,
|
||||
@@ -50,12 +50,13 @@ public func createContactConnectedNtf(_ contact: Contact) -> UNMutableNotificati
|
||||
NSLocalizedString("You can now send messages to %@", comment: "notification body"),
|
||||
hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName
|
||||
),
|
||||
targetContentIdentifier: contact.id
|
||||
targetContentIdentifier: contact.id,
|
||||
userInfo: ["userId": user.userId]
|
||||
// userInfo: ["chatId": contact.id, "contactId": contact.apiId]
|
||||
)
|
||||
}
|
||||
|
||||
public func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
|
||||
public func createMessageReceivedNtf(_ user: User, _ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
|
||||
let previewMode = ntfPreviewModeGroupDefault.get()
|
||||
var title: String
|
||||
if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir {
|
||||
@@ -67,7 +68,8 @@ public func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UN
|
||||
categoryIdentifier: ntfCategoryMessageReceived,
|
||||
title: title,
|
||||
body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"),
|
||||
targetContentIdentifier: cInfo.id
|
||||
targetContentIdentifier: cInfo.id,
|
||||
userInfo: ["userId": user.userId]
|
||||
// userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id]
|
||||
)
|
||||
}
|
||||
@@ -82,11 +84,11 @@ public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutabl
|
||||
title: hideContent ? contactHidden : "\(invitation.contact.chatViewName):",
|
||||
body: text,
|
||||
targetContentIdentifier: nil,
|
||||
userInfo: ["chatId": invitation.contact.id]
|
||||
userInfo: ["chatId": invitation.contact.id, "userId": invitation.user.userId]
|
||||
)
|
||||
}
|
||||
|
||||
public func createConnectionEventNtf(_ connEntity: ConnectionEntity) -> UNMutableNotificationContent {
|
||||
public func createConnectionEventNtf(_ user: User, _ connEntity: ConnectionEntity) -> UNMutableNotificationContent {
|
||||
let hideContent = ntfPreviewModeGroupDefault.get() == .hidden
|
||||
var title: String
|
||||
var body: String? = nil
|
||||
@@ -115,7 +117,8 @@ public func createConnectionEventNtf(_ connEntity: ConnectionEntity) -> UNMutabl
|
||||
categoryIdentifier: ntfCategoryConnectionEvent,
|
||||
title: title,
|
||||
body: body,
|
||||
targetContentIdentifier: targetContentIdentifier
|
||||
targetContentIdentifier: targetContentIdentifier,
|
||||
userInfo: ["userId": user.userId]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,10 @@ mySquaringBot _user cc = do
|
||||
race_ (forever $ void getLine) . forever $ do
|
||||
(_, resp) <- atomically . readTBQueue $ outputQ cc
|
||||
case resp of
|
||||
CRContactConnected contact _ -> do
|
||||
CRContactConnected _ contact _ -> do
|
||||
contactConnected contact
|
||||
void . sendMsg contact $ "Hello! I am a simple squaring bot - if you send me a number, I will calculate its square"
|
||||
CRNewChatItem (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content}) -> do
|
||||
CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content}) -> do
|
||||
let msg = T.unpack $ ciContentToText content
|
||||
number_ = readMaybe msg :: Maybe Integer
|
||||
void . sendMsg contact $ case number_ of
|
||||
|
||||
@@ -95,11 +95,11 @@ runChatServer ChatServerConfig {chatPort, clientQSize} cc = do
|
||||
Left e -> sendError (Just corrId) e
|
||||
Nothing -> sendError Nothing "invalid request"
|
||||
where
|
||||
sendError corrId e = atomically $ writeTBQueue sndQ ChatSrvResponse {corrId, resp = chatCmdError e}
|
||||
sendError corrId e = atomically $ writeTBQueue sndQ ChatSrvResponse {corrId, resp = chatCmdError Nothing e}
|
||||
processCommand (corrId, cmd) =
|
||||
runReaderT (runExceptT $ processChatCommand cmd) cc >>= \case
|
||||
Right resp -> response resp
|
||||
Left e -> response $ CRChatCmdError e
|
||||
Left e -> response $ CRChatCmdError Nothing e
|
||||
where
|
||||
response resp = pure ChatSrvResponse {corrId = Just corrId, resp}
|
||||
clientDisconnected _ = pure ()
|
||||
|
||||
+6
-1
@@ -7,7 +7,12 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 56eea29ec36ba723fda6ae27a18e03a4e5aad6e8
|
||||
tag: b59669a65eafeb92d4a986a03e481ecb343ee307
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/hs-socks.git
|
||||
tag: a30cc7a79a08d8108316094f8f2f82a0c5e1ac51
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -450,7 +450,7 @@ export function cmdString(cmd: ChatCommand): string {
|
||||
case "showActiveUser":
|
||||
return "/u"
|
||||
case "createActiveUser":
|
||||
return `/u ${JSON.stringify(cmd.profile)}`
|
||||
return `/create user ${JSON.stringify(cmd.profile)}`
|
||||
case "startChat":
|
||||
return `/_start subscribe=${cmd.subscribeConnections ? "on" : "off"} expire=${cmd.expireChatItems ? "on" : "off"}`
|
||||
case "apiStopChat":
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."56eea29ec36ba723fda6ae27a18e03a4e5aad6e8" = "107vy6147vf282w41kmm6jhhiykcg4jz9napav5m0kcfinzbkqpg";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."b59669a65eafeb92d4a986a03e481ecb343ee307" = "0h4kgnlg0hy5dy5svshj6fhz277rqls5w1h95b1vg719xxcc2n8m";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0";
|
||||
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
|
||||
|
||||
@@ -79,6 +79,9 @@ library
|
||||
Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status
|
||||
Simplex.Chat.Migrations.M20221230_idxs
|
||||
Simplex.Chat.Migrations.M20230107_connections_auth_err_counter
|
||||
Simplex.Chat.Migrations.M20230111_users_agent_user_id
|
||||
Simplex.Chat.Migrations.M20230117_fkey_indexes
|
||||
Simplex.Chat.Migrations.M20230118_recreate_smp_servers
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Options
|
||||
Simplex.Chat.ProfileGenerator
|
||||
|
||||
+712
-520
File diff suppressed because it is too large
Load Diff
@@ -24,10 +24,10 @@ chatBotRepl welcome answer _user cc = do
|
||||
race_ (forever $ void getLine) . forever $ do
|
||||
(_, resp) <- atomically . readTBQueue $ outputQ cc
|
||||
case resp of
|
||||
CRContactConnected contact _ -> do
|
||||
CRContactConnected _ contact _ -> do
|
||||
contactConnected contact
|
||||
void $ sendMsg contact welcome
|
||||
CRNewChatItem (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content}) -> do
|
||||
CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content}) -> do
|
||||
let msg = T.unpack $ ciContentToText content
|
||||
void . sendMsg contact $ answer msg
|
||||
_ -> pure ()
|
||||
@@ -38,11 +38,11 @@ chatBotRepl welcome answer _user cc = do
|
||||
initializeBotAddress :: ChatController -> IO ()
|
||||
initializeBotAddress cc = do
|
||||
sendChatCmd cc "/show_address" >>= \case
|
||||
CRUserContactLink UserContactLink {connReqContact} -> showBotAddress connReqContact
|
||||
CRChatCmdError (ChatErrorStore SEUserContactLinkNotFound) -> do
|
||||
CRUserContactLink _ UserContactLink {connReqContact} -> showBotAddress connReqContact
|
||||
CRChatCmdError _ (ChatErrorStore SEUserContactLinkNotFound) -> do
|
||||
putStrLn "No bot address, creating..."
|
||||
sendChatCmd cc "/address" >>= \case
|
||||
CRUserContactLinkCreated uri -> showBotAddress uri
|
||||
CRUserContactLinkCreated _ uri -> showBotAddress uri
|
||||
_ -> putStrLn "can't create bot address" >> exitFailure
|
||||
_ -> putStrLn "unexpected response" >> exitFailure
|
||||
where
|
||||
|
||||
@@ -21,7 +21,7 @@ import Data.Time.Clock (UTCTime)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Chat.Types (Contact, ContactId, decodeJSON, encodeJSON)
|
||||
import Simplex.Chat.Types (Contact, ContactId, decodeJSON, encodeJSON, User)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON)
|
||||
@@ -125,7 +125,8 @@ instance FromField CallId where fromField f = CallId <$> fromField f
|
||||
instance ToField CallId where toField (CallId m) = toField m
|
||||
|
||||
data RcvCallInvitation = RcvCallInvitation
|
||||
{ contact :: Contact,
|
||||
{ user :: User,
|
||||
contact :: Contact,
|
||||
callType :: CallType,
|
||||
sharedKey :: Maybe C.Key,
|
||||
callTs :: UTCTime
|
||||
|
||||
+180
-136
@@ -45,7 +45,7 @@ import Simplex.Chat.Store (AutoAccept, StoreError, UserContactLink)
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Agent (AgentClient)
|
||||
import Simplex.Messaging.Agent.Client (AgentLocks, SMPTestFailure)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, InitialAgentServers, NetworkConfig)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig)
|
||||
import Simplex.Messaging.Agent.Lock
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore)
|
||||
@@ -53,7 +53,7 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (AProtocolType, CorrId, MsgFlags)
|
||||
import Simplex.Messaging.Protocol (AProtocolType, CorrId, MsgFlags, NtfServer, QueueId)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import Simplex.Messaging.Transport (simplexMQVersion)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
@@ -101,7 +101,7 @@ coreVersionInfo buildTimestamp simplexmqCommit =
|
||||
data ChatConfig = ChatConfig
|
||||
{ agentConfig :: AgentConfig,
|
||||
yesToMigrations :: Bool,
|
||||
defaultServers :: InitialAgentServers,
|
||||
defaultServers :: DefaultAgentServers,
|
||||
tbqSize :: Natural,
|
||||
fileChunkSize :: Integer,
|
||||
inlineFiles :: InlineFilesConfig,
|
||||
@@ -109,7 +109,14 @@ data ChatConfig = ChatConfig
|
||||
subscriptionEvents :: Bool,
|
||||
hostEvents :: Bool,
|
||||
logLevel :: ChatLogLevel,
|
||||
testView :: Bool
|
||||
testView :: Bool,
|
||||
ciExpirationInterval :: Int -- microseconds
|
||||
}
|
||||
|
||||
data DefaultAgentServers = DefaultAgentServers
|
||||
{ smp :: NonEmpty SMPServerWithAuth,
|
||||
ntf :: [NtfServer],
|
||||
netCfg :: NetworkConfig
|
||||
}
|
||||
|
||||
data InlineFilesConfig = InlineFilesConfig
|
||||
@@ -155,8 +162,8 @@ data ChatController = ChatController
|
||||
config :: ChatConfig,
|
||||
filesFolder :: TVar (Maybe FilePath), -- path to files folder for mobile apps,
|
||||
incognitoMode :: TVar Bool,
|
||||
expireCIsAsync :: TVar (Maybe (Async ())),
|
||||
expireCIs :: TVar Bool,
|
||||
expireCIThreads :: TMap UserId (Maybe (Async ())),
|
||||
expireCIFlags :: TMap UserId Bool,
|
||||
cleanupManagerAsync :: TVar (Maybe (Async ())),
|
||||
timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))),
|
||||
showLiveItems :: TVar Bool
|
||||
@@ -171,7 +178,12 @@ instance ToJSON HelpSection where
|
||||
|
||||
data ChatCommand
|
||||
= ShowActiveUser
|
||||
| CreateActiveUser Profile
|
||||
| CreateActiveUser Profile Bool
|
||||
| ListUsers
|
||||
| APISetActiveUser UserId
|
||||
| SetActiveUser UserName
|
||||
| APIDeleteUser UserId Bool
|
||||
| DeleteUser UserName Bool
|
||||
| StartChat {subscribeConnections :: Bool, enableExpireChatItems :: Bool}
|
||||
| APIStopChat
|
||||
| APIActivateChat
|
||||
@@ -185,7 +197,7 @@ data ChatCommand
|
||||
| APIStorageEncryption DBEncryptionConfig
|
||||
| ExecChatStoreSQL Text
|
||||
| ExecAgentStoreSQL Text
|
||||
| APIGetChats {pendingConnections :: Bool}
|
||||
| APIGetChats {userId :: UserId, pendingConnections :: Bool}
|
||||
| APIGetChat ChatRef ChatPagination (Maybe String)
|
||||
| APIGetChatItems Int
|
||||
| APISendMessage {chatRef :: ChatRef, liveMessage :: Bool, composedMessage :: ComposedMessage}
|
||||
@@ -206,7 +218,7 @@ data ChatCommand
|
||||
| APIEndCall ContactId
|
||||
| APIGetCallInvitations
|
||||
| APICallStatus ContactId WebRTCCallStatus
|
||||
| APIUpdateProfile Profile
|
||||
| APIUpdateProfile UserId Profile
|
||||
| APISetContactPrefs ContactId Preferences
|
||||
| APISetContactAlias ContactId LocalAlias
|
||||
| APISetConnectionAlias Int64 LocalAlias
|
||||
@@ -226,11 +238,15 @@ data ChatCommand
|
||||
| APICreateGroupLink GroupId
|
||||
| APIDeleteGroupLink GroupId
|
||||
| APIGetGroupLink GroupId
|
||||
| APIGetUserSMPServers UserId
|
||||
| GetUserSMPServers
|
||||
| APISetUserSMPServers UserId SMPServersConfig
|
||||
| SetUserSMPServers SMPServersConfig
|
||||
| TestSMPServer SMPServerWithAuth
|
||||
| APISetChatItemTTL (Maybe Int64)
|
||||
| APIGetChatItemTTL
|
||||
| TestSMPServer UserId SMPServerWithAuth
|
||||
| APISetChatItemTTL UserId (Maybe Int64)
|
||||
| SetChatItemTTL (Maybe Int64)
|
||||
| APIGetChatItemTTL UserId
|
||||
| GetChatItemTTL
|
||||
| APISetNetworkConfig NetworkConfig
|
||||
| APIGetNetworkConfig
|
||||
| APISetChatSettings ChatRef ChatSettings
|
||||
@@ -257,25 +273,33 @@ data ChatCommand
|
||||
| EnableGroupMember GroupName ContactName
|
||||
| ChatHelp HelpSection
|
||||
| Welcome
|
||||
| APIAddContact UserId
|
||||
| AddContact
|
||||
| APIConnect UserId (Maybe AConnectionRequestUri)
|
||||
| Connect (Maybe AConnectionRequestUri)
|
||||
| ConnectSimplex
|
||||
| ConnectSimplex -- UserId (not used in UI)
|
||||
| DeleteContact ContactName
|
||||
| ClearContact ContactName
|
||||
| APIListContacts UserId
|
||||
| ListContacts
|
||||
| APICreateMyAddress UserId
|
||||
| CreateMyAddress
|
||||
| APIDeleteMyAddress UserId
|
||||
| DeleteMyAddress
|
||||
| APIShowMyAddress UserId
|
||||
| ShowMyAddress
|
||||
| APIAddressAutoAccept UserId (Maybe AutoAccept)
|
||||
| AddressAutoAccept (Maybe AutoAccept)
|
||||
| AcceptContact ContactName
|
||||
| RejectContact ContactName
|
||||
| SendMessage ChatName ByteString
|
||||
| SendLiveMessage ChatName ByteString
|
||||
| SendMessageQuote {contactName :: ContactName, msgDir :: AMsgDirection, quotedMsg :: ByteString, message :: ByteString}
|
||||
| SendMessageBroadcast ByteString
|
||||
| SendMessageBroadcast ByteString -- UserId (not used in UI)
|
||||
| DeleteMessage ChatName ByteString
|
||||
| EditMessage {chatName :: ChatName, editedMsg :: ByteString, message :: ByteString}
|
||||
| UpdateLiveMessage {chatName :: ChatName, chatItemId :: ChatItemId, liveMessage :: Bool, message :: ByteString}
|
||||
| APINewGroup UserId GroupProfile
|
||||
| NewGroup GroupProfile
|
||||
| AddMember GroupName ContactName GroupMemberRole
|
||||
| JoinGroup GroupName
|
||||
@@ -285,7 +309,7 @@ data ChatCommand
|
||||
| DeleteGroup GroupName
|
||||
| ClearGroup GroupName
|
||||
| ListMembers GroupName
|
||||
| ListGroups
|
||||
| ListGroups -- UserId (not used in UI)
|
||||
| UpdateGroupNames GroupName GroupProfile
|
||||
| ShowGroupProfile GroupName
|
||||
| UpdateGroupDescription GroupName (Maybe Text)
|
||||
@@ -293,10 +317,10 @@ data ChatCommand
|
||||
| DeleteGroupLink GroupName
|
||||
| ShowGroupLink GroupName
|
||||
| SendGroupMessageQuote {groupName :: GroupName, contactName_ :: Maybe ContactName, quotedMsg :: ByteString, message :: ByteString}
|
||||
| LastChats (Maybe Int)
|
||||
| LastMessages (Maybe ChatName) Int (Maybe String)
|
||||
| LastChatItemId (Maybe ChatName) Int
|
||||
| ShowChatItem (Maybe ChatItemId)
|
||||
| LastChats (Maybe Int) -- UserId (not used in UI)
|
||||
| LastMessages (Maybe ChatName) Int (Maybe String) -- UserId (not used in UI)
|
||||
| LastChatItemId (Maybe ChatName) Int -- UserId (not used in UI)
|
||||
| ShowChatItem (Maybe ChatItemId) -- UserId (not used in UI)
|
||||
| ShowLiveItems Bool
|
||||
| SendFile ChatName FilePath
|
||||
| SendImage ChatName FilePath
|
||||
@@ -305,13 +329,13 @@ data ChatCommand
|
||||
| ReceiveFile {fileId :: FileTransferId, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
|
||||
| CancelFile FileTransferId
|
||||
| FileStatus FileTransferId
|
||||
| ShowProfile
|
||||
| UpdateProfile ContactName Text
|
||||
| UpdateProfileImage (Maybe ImageData)
|
||||
| SetUserFeature AChatFeature FeatureAllowed
|
||||
| ShowProfile -- UserId (not used in UI)
|
||||
| UpdateProfile ContactName Text -- UserId (not used in UI)
|
||||
| UpdateProfileImage (Maybe ImageData) -- UserId (not used in UI)
|
||||
| SetUserFeature AChatFeature FeatureAllowed -- UserId (not used in UI)
|
||||
| SetContactFeature AChatFeature ContactName (Maybe FeatureAllowed)
|
||||
| SetGroupFeature AGroupFeature GroupName GroupFeatureEnabled
|
||||
| SetUserTimedMessages Bool
|
||||
| SetUserTimedMessages Bool -- UserId (not used in UI)
|
||||
| SetContactTimedMessages ContactName (Maybe TimedMessagesEnabled)
|
||||
| SetGroupTimedMessages GroupName (Maybe Int)
|
||||
| QuitChat
|
||||
@@ -323,145 +347,161 @@ data ChatCommand
|
||||
|
||||
data ChatResponse
|
||||
= CRActiveUser {user :: User}
|
||||
| CRUsersList {users :: [UserInfo]}
|
||||
| CRChatStarted
|
||||
| CRChatRunning
|
||||
| CRChatStopped
|
||||
| CRChatSuspended
|
||||
| CRApiChats {chats :: [AChat]}
|
||||
| CRApiChats {user :: User, chats :: [AChat]}
|
||||
| CRChats {chats :: [AChat]}
|
||||
| CRApiChat {chat :: AChat}
|
||||
| CRChatItems {chatItems :: [AChatItem]}
|
||||
| CRChatItemId (Maybe ChatItemId)
|
||||
| CRApiChat {user :: User, chat :: AChat}
|
||||
| CRChatItems {user :: User, chatItems :: [AChatItem]}
|
||||
| CRChatItemId User (Maybe ChatItemId)
|
||||
| CRApiParsedMarkdown {formattedText :: Maybe MarkdownList}
|
||||
| CRUserSMPServers {smpServers :: NonEmpty ServerCfg, presetSMPServers :: NonEmpty SMPServerWithAuth}
|
||||
| CRSmpTestResult {smpTestFailure :: Maybe SMPTestFailure}
|
||||
| CRChatItemTTL {chatItemTTL :: Maybe Int64}
|
||||
| CRUserSMPServers {user :: User, smpServers :: NonEmpty ServerCfg, presetSMPServers :: NonEmpty SMPServerWithAuth}
|
||||
| CRSmpTestResult {user :: User, smpTestFailure :: Maybe SMPTestFailure}
|
||||
| CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64}
|
||||
| CRNetworkConfig {networkConfig :: NetworkConfig}
|
||||
| CRContactInfo {contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile}
|
||||
| CRGroupMemberInfo {groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats}
|
||||
| CRContactSwitch {contact :: Contact, switchProgress :: SwitchProgress}
|
||||
| CRGroupMemberSwitch {groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress}
|
||||
| CRContactCode {contact :: Contact, connectionCode :: Text}
|
||||
| CRGroupMemberCode {groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text}
|
||||
| CRConnectionVerified {verified :: Bool, expectedCode :: Text}
|
||||
| CRNewChatItem {chatItem :: AChatItem}
|
||||
| CRChatItemStatusUpdated {chatItem :: AChatItem}
|
||||
| CRChatItemUpdated {chatItem :: AChatItem}
|
||||
| CRChatItemDeleted {deletedChatItem :: AChatItem, toChatItem :: Maybe AChatItem, byUser :: Bool, timed :: Bool}
|
||||
| CRChatItemDeletedNotFound {contact :: Contact, sharedMsgId :: SharedMsgId}
|
||||
| CRBroadcastSent MsgContent Int ZonedTime
|
||||
| CRMsgIntegrityError {msgError :: MsgErrorType}
|
||||
| CRContactInfo {user :: User, contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile}
|
||||
| CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats}
|
||||
| CRContactSwitch {user :: User, contact :: Contact, switchProgress :: SwitchProgress}
|
||||
| CRGroupMemberSwitch {user :: User, groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress}
|
||||
| CRContactCode {user :: User, contact :: Contact, connectionCode :: Text}
|
||||
| CRGroupMemberCode {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text}
|
||||
| CRConnectionVerified {user :: User, verified :: Bool, expectedCode :: Text}
|
||||
| CRNewChatItem {user :: User, chatItem :: AChatItem}
|
||||
| CRChatItemStatusUpdated {user :: User, chatItem :: AChatItem}
|
||||
| CRChatItemUpdated {user :: User, chatItem :: AChatItem}
|
||||
| CRChatItemDeleted {user :: User, deletedChatItem :: AChatItem, toChatItem :: Maybe AChatItem, byUser :: Bool, timed :: Bool}
|
||||
| CRChatItemDeletedNotFound {user :: User, contact :: Contact, sharedMsgId :: SharedMsgId}
|
||||
| CRBroadcastSent User MsgContent Int ZonedTime
|
||||
| CRMsgIntegrityError {user :: User, msgError :: MsgErrorType}
|
||||
| CRCmdAccepted {corr :: CorrId}
|
||||
| CRCmdOk
|
||||
| CRCmdOk {user_ :: Maybe User}
|
||||
| CRChatHelp {helpSection :: HelpSection}
|
||||
| CRWelcome {user :: User}
|
||||
| CRGroupCreated {groupInfo :: GroupInfo}
|
||||
| CRGroupMembers {group :: Group}
|
||||
| CRContactsList {contacts :: [Contact]}
|
||||
| CRUserContactLink {contactLink :: UserContactLink}
|
||||
| CRUserContactLinkUpdated {contactLink :: UserContactLink}
|
||||
| CRContactRequestRejected {contactRequest :: UserContactRequest}
|
||||
| CRUserAcceptedGroupSent {groupInfo :: GroupInfo, hostContact :: Maybe Contact}
|
||||
| CRUserDeletedMember {groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRGroupsList {groups :: [GroupInfo]}
|
||||
| CRSentGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember}
|
||||
| CRFileTransferStatus (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus
|
||||
| CRUserProfile {profile :: Profile}
|
||||
| CRUserProfileNoChange
|
||||
| CRGroupCreated {user :: User, groupInfo :: GroupInfo}
|
||||
| CRGroupMembers {user :: User, group :: Group}
|
||||
| CRContactsList {user :: User, contacts :: [Contact]}
|
||||
| CRUserContactLink {user :: User, contactLink :: UserContactLink}
|
||||
| CRUserContactLinkUpdated {user :: User, contactLink :: UserContactLink}
|
||||
| CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest}
|
||||
| CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact}
|
||||
| CRUserDeletedMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRGroupsList {user :: User, groups :: [GroupInfo]}
|
||||
| CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember}
|
||||
| CRFileTransferStatus User (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus
|
||||
| CRUserProfile {user :: User, profile :: Profile}
|
||||
| CRUserProfileNoChange {user :: User}
|
||||
| CRVersionInfo {versionInfo :: CoreVersionInfo}
|
||||
| CRInvitation {connReqInvitation :: ConnReqInvitation}
|
||||
| CRSentConfirmation
|
||||
| CRSentInvitation {customUserProfile :: Maybe Profile}
|
||||
| CRContactUpdated {fromContact :: Contact, toContact :: Contact}
|
||||
| CRContactsMerged {intoContact :: Contact, mergedContact :: Contact}
|
||||
| CRContactDeleted {contact :: Contact}
|
||||
| CRChatCleared {chatInfo :: AChatInfo}
|
||||
| CRUserContactLinkCreated {connReqContact :: ConnReqContact}
|
||||
| CRUserContactLinkDeleted
|
||||
| CRReceivedContactRequest {contactRequest :: UserContactRequest}
|
||||
| CRAcceptingContactRequest {contact :: Contact}
|
||||
| CRContactAlreadyExists {contact :: Contact}
|
||||
| CRContactRequestAlreadyAccepted {contact :: Contact}
|
||||
| CRLeftMemberUser {groupInfo :: GroupInfo}
|
||||
| CRGroupDeletedUser {groupInfo :: GroupInfo}
|
||||
| CRRcvFileAccepted {chatItem :: AChatItem}
|
||||
| CRRcvFileAcceptedSndCancelled {rcvFileTransfer :: RcvFileTransfer}
|
||||
| CRRcvFileStart {chatItem :: AChatItem}
|
||||
| CRRcvFileComplete {chatItem :: AChatItem}
|
||||
| CRRcvFileCancelled {rcvFileTransfer :: RcvFileTransfer}
|
||||
| CRRcvFileSndCancelled {rcvFileTransfer :: RcvFileTransfer}
|
||||
| CRSndFileStart {chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||
| CRSndFileComplete {chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||
| CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation}
|
||||
| CRSentConfirmation {user :: User}
|
||||
| CRSentInvitation {user :: User, customUserProfile :: Maybe Profile}
|
||||
| CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
|
||||
| CRContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact}
|
||||
| CRContactDeleted {user :: User, contact :: Contact}
|
||||
| CRChatCleared {user :: User, chatInfo :: AChatInfo}
|
||||
| CRUserContactLinkCreated {user :: User, connReqContact :: ConnReqContact}
|
||||
| CRUserContactLinkDeleted {user :: User}
|
||||
| CRReceivedContactRequest {user :: User, contactRequest :: UserContactRequest}
|
||||
| CRAcceptingContactRequest {user :: User, contact :: Contact}
|
||||
| CRContactAlreadyExists {user :: User, contact :: Contact}
|
||||
| CRContactRequestAlreadyAccepted {user :: User, contact :: Contact}
|
||||
| CRLeftMemberUser {user :: User, groupInfo :: GroupInfo}
|
||||
| CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo}
|
||||
| CRRcvFileAccepted {user :: User, chatItem :: AChatItem}
|
||||
| CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
|
||||
| CRRcvFileStart {user :: User, chatItem :: AChatItem}
|
||||
| CRRcvFileComplete {user :: User, chatItem :: AChatItem}
|
||||
| CRRcvFileCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
|
||||
| CRRcvFileSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
|
||||
| CRSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||
| CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||
| CRSndFileCancelled {chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||
| CRSndFileRcvCancelled {chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||
| CRSndGroupFileCancelled {chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]}
|
||||
| CRUserProfileUpdated {fromProfile :: Profile, toProfile :: Profile}
|
||||
| CRContactAliasUpdated {toContact :: Contact}
|
||||
| CRConnectionAliasUpdated {toConnection :: PendingContactConnection}
|
||||
| CRContactPrefsUpdated {fromContact :: Contact, toContact :: Contact}
|
||||
| CRContactConnecting {contact :: Contact}
|
||||
| CRContactConnected {contact :: Contact, userCustomProfile :: Maybe Profile}
|
||||
| CRContactAnotherClient {contact :: Contact}
|
||||
| CRSubscriptionEnd {connectionEntity :: ConnectionEntity}
|
||||
| CRSndFileRcvCancelled {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||
| CRSndGroupFileCancelled {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta, sndFileTransfers :: [SndFileTransfer]}
|
||||
| CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile}
|
||||
| CRContactAliasUpdated {user :: User, toContact :: Contact}
|
||||
| CRConnectionAliasUpdated {user :: User, toConnection :: PendingContactConnection}
|
||||
| CRContactPrefsUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
|
||||
| CRContactConnecting {user :: User, contact :: Contact}
|
||||
| CRContactConnected {user :: User, contact :: Contact, userCustomProfile :: Maybe Profile}
|
||||
| CRContactAnotherClient {user :: User, contact :: Contact}
|
||||
| CRSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity}
|
||||
| CRContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]}
|
||||
| CRContactsSubscribed {server :: SMPServer, contactRefs :: [ContactRef]}
|
||||
| CRContactSubError {contact :: Contact, chatError :: ChatError}
|
||||
| CRContactSubSummary {contactSubscriptions :: [ContactSubStatus]}
|
||||
| CRUserContactSubSummary {userContactSubscriptions :: [UserContactSubStatus]}
|
||||
| CRContactSubError {contact :: Contact, chatError :: ChatError} -- TODO delete
|
||||
| CRContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]}
|
||||
| CRUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]}
|
||||
| CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
|
||||
| CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost}
|
||||
| CRGroupInvitation {groupInfo :: GroupInfo}
|
||||
| CRReceivedGroupInvitation {groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole}
|
||||
| CRUserJoinedGroup {groupInfo :: GroupInfo, hostMember :: GroupMember}
|
||||
| CRJoinedGroupMember {groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRJoinedGroupMemberConnecting {groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember}
|
||||
| CRMemberRole {groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole}
|
||||
| CRMemberRoleUser {groupInfo :: GroupInfo, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole}
|
||||
| CRConnectedToGroupMember {groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRDeletedMember {groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember}
|
||||
| CRDeletedMemberUser {groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRLeftMember {groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRGroupEmpty {groupInfo :: GroupInfo}
|
||||
| CRGroupRemoved {groupInfo :: GroupInfo}
|
||||
| CRGroupDeleted {groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRGroupUpdated {fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember}
|
||||
| CRGroupProfile {groupInfo :: GroupInfo}
|
||||
| CRGroupLinkCreated {groupInfo :: GroupInfo, connReqContact :: ConnReqContact}
|
||||
| CRGroupLink {groupInfo :: GroupInfo, connReqContact :: ConnReqContact}
|
||||
| CRGroupLinkDeleted {groupInfo :: GroupInfo}
|
||||
| CRAcceptingGroupJoinRequest {groupInfo :: GroupInfo, contact :: Contact}
|
||||
| CRMemberSubError {groupInfo :: GroupInfo, member :: GroupMember, chatError :: ChatError}
|
||||
| CRMemberSubSummary {memberSubscriptions :: [MemberSubStatus]}
|
||||
| CRGroupSubscribed {groupInfo :: GroupInfo}
|
||||
| CRPendingSubSummary {pendingSubscriptions :: [PendingSubStatus]}
|
||||
| CRSndFileSubError {sndFileTransfer :: SndFileTransfer, chatError :: ChatError}
|
||||
| CRRcvFileSubError {rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError}
|
||||
| CRGroupInvitation {user :: User, groupInfo :: GroupInfo}
|
||||
| CRReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole}
|
||||
| CRUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember}
|
||||
| CRJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRJoinedGroupMemberConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember}
|
||||
| CRMemberRole {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole}
|
||||
| CRMemberRoleUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole}
|
||||
| CRConnectedToGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRDeletedMember {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, deletedMember :: GroupMember}
|
||||
| CRDeletedMemberUser {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRLeftMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRGroupEmpty {user :: User, groupInfo :: GroupInfo}
|
||||
| CRGroupRemoved {user :: User, groupInfo :: GroupInfo}
|
||||
| CRGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember}
|
||||
| CRGroupProfile {user :: User, groupInfo :: GroupInfo}
|
||||
| CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact}
|
||||
| CRGroupLink {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact}
|
||||
| CRGroupLinkDeleted {user :: User, groupInfo :: GroupInfo}
|
||||
| CRAcceptingGroupJoinRequest {user :: User, groupInfo :: GroupInfo, contact :: Contact}
|
||||
| CRMemberSubError {user :: User, groupInfo :: GroupInfo, member :: GroupMember, chatError :: ChatError}
|
||||
| CRMemberSubSummary {user :: User, memberSubscriptions :: [MemberSubStatus]}
|
||||
| CRGroupSubscribed {user :: User, groupInfo :: GroupInfo}
|
||||
| CRPendingSubSummary {user :: User, pendingSubscriptions :: [PendingSubStatus]}
|
||||
| CRSndFileSubError {user :: User, sndFileTransfer :: SndFileTransfer, chatError :: ChatError}
|
||||
| CRRcvFileSubError {user :: User, rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError}
|
||||
| CRCallInvitation {callInvitation :: RcvCallInvitation}
|
||||
| CRCallOffer {contact :: Contact, callType :: CallType, offer :: WebRTCSession, sharedKey :: Maybe C.Key, askConfirmation :: Bool}
|
||||
| CRCallAnswer {contact :: Contact, answer :: WebRTCSession}
|
||||
| CRCallExtraInfo {contact :: Contact, extraInfo :: WebRTCExtraInfo}
|
||||
| CRCallEnded {contact :: Contact}
|
||||
| CRCallOffer {user :: User, contact :: Contact, callType :: CallType, offer :: WebRTCSession, sharedKey :: Maybe C.Key, askConfirmation :: Bool}
|
||||
| CRCallAnswer {user :: User, contact :: Contact, answer :: WebRTCSession}
|
||||
| CRCallExtraInfo {user :: User, contact :: Contact, extraInfo :: WebRTCExtraInfo}
|
||||
| CRCallEnded {user :: User, contact :: Contact}
|
||||
| CRCallInvitations {callInvitations :: [RcvCallInvitation]}
|
||||
| CRUserContactLinkSubscribed
|
||||
| CRUserContactLinkSubError {chatError :: ChatError}
|
||||
| CRUserContactLinkSubscribed -- TODO delete
|
||||
| CRUserContactLinkSubError {chatError :: ChatError} -- TODO delete
|
||||
| CRNtfTokenStatus {status :: NtfTknStatus}
|
||||
| CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode}
|
||||
| CRNtfMessages {connEntity :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]}
|
||||
| CRNewContactConnection {connection :: PendingContactConnection}
|
||||
| CRContactConnectionDeleted {connection :: PendingContactConnection}
|
||||
| CRNtfMessages {user_ :: Maybe User, connEntity :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]}
|
||||
| CRNewContactConnection {user :: User, connection :: PendingContactConnection}
|
||||
| CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection}
|
||||
| CRSQLResult {rows :: [Text]}
|
||||
| CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks}
|
||||
| CRAgentStats {agentStats :: [[String]]}
|
||||
| CRConnectionDisabled {connectionEntity :: ConnectionEntity}
|
||||
| CRMessageError {severity :: Text, errorMessage :: Text}
|
||||
| CRChatCmdError {chatError :: ChatError}
|
||||
| CRChatError {chatError :: ChatError}
|
||||
| CRAgentRcvQueueDeleted {agentConnId :: AgentConnId, server :: SMPServer, agentQueueId :: AgentQueueId, agentError_ :: Maybe AgentErrorType}
|
||||
| CRAgentConnDeleted {agentConnId :: AgentConnId}
|
||||
| CRAgentUserDeleted {agentUserId :: Int64}
|
||||
| CRMessageError {user :: User, severity :: Text, errorMessage :: Text}
|
||||
| CRChatCmdError {user_ :: Maybe User, chatError :: ChatError}
|
||||
| CRChatError {user_ :: Maybe User, chatError :: ChatError}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ToJSON ChatResponse where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR"
|
||||
|
||||
newtype AgentQueueId = AgentQueueId QueueId
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding AgentQueueId where
|
||||
strEncode (AgentQueueId qId) = strEncode qId
|
||||
strDecode s = AgentQueueId <$> strDecode s
|
||||
strP = AgentQueueId <$> strP
|
||||
|
||||
instance ToJSON AgentQueueId where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
data SMPServersConfig = SMPServersConfig {smpServers :: [ServerCfg]}
|
||||
deriving (Show, Generic, FromJSON)
|
||||
|
||||
@@ -603,7 +643,11 @@ instance ToJSON ChatError where
|
||||
|
||||
data ChatErrorType
|
||||
= CENoActiveUser
|
||||
| CEActiveUserExists
|
||||
| CENoConnectionUser {agentConnId :: AgentConnId}
|
||||
| CEActiveUserExists -- TODO delete
|
||||
| CEDifferentActiveUser {commandUserId :: UserId, activeUserId :: UserId}
|
||||
| CECantDeleteActiveUser {userId :: UserId}
|
||||
| CECantDeleteLastUser {userId :: UserId}
|
||||
| CEChatNotStarted
|
||||
| CEChatNotStopped
|
||||
| CEChatStoreChanged
|
||||
@@ -681,8 +725,8 @@ throwDBError = throwError . ChatErrorDatabase
|
||||
|
||||
type ChatMonad m = (MonadUnliftIO m, MonadReader ChatController m, MonadError ChatError m)
|
||||
|
||||
chatCmdError :: String -> ChatResponse
|
||||
chatCmdError = CRChatCmdError . ChatError . CECommandError
|
||||
chatCmdError :: Maybe User -> String -> ChatResponse
|
||||
chatCmdError user = CRChatCmdError user . ChatError . CECommandError
|
||||
|
||||
setActive :: (MonadUnliftIO m, MonadReader ChatController m) => ActiveTo -> m ()
|
||||
setActive to = asks activeTo >>= atomically . (`writeTVar` to)
|
||||
|
||||
@@ -30,7 +30,7 @@ runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController
|
||||
runSimplexChat ChatOpts {maintenance} u cc chat
|
||||
| maintenance = wait =<< async (chat u cc)
|
||||
| otherwise = do
|
||||
a1 <- runReaderT (startChatController u True True) cc
|
||||
a1 <- runReaderT (startChatController True True) cc
|
||||
a2 <- async $ chat u cc
|
||||
waitEither_ a1 a2
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20230111_users_agent_user_id where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20230111_users_agent_user_id :: Query
|
||||
m20230111_users_agent_user_id =
|
||||
[sql|
|
||||
PRAGMA ignore_check_constraints=ON;
|
||||
|
||||
ALTER TABLE users ADD COLUMN agent_user_id INTEGER CHECK (agent_user_id NOT NULL);
|
||||
UPDATE users SET agent_user_id = 1;
|
||||
|
||||
PRAGMA ignore_check_constraints=OFF;
|
||||
|]
|
||||
@@ -0,0 +1,59 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20230117_fkey_indexes where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
-- .lint fkey-indexes
|
||||
m20230117_fkey_indexes :: Query
|
||||
m20230117_fkey_indexes =
|
||||
[sql|
|
||||
CREATE INDEX idx_calls_user_id ON calls(user_id);
|
||||
CREATE INDEX idx_calls_chat_item_id ON calls(chat_item_id);
|
||||
CREATE INDEX idx_calls_contact_id ON calls(contact_id);
|
||||
CREATE INDEX idx_chat_items_group_id ON chat_items(group_id);
|
||||
CREATE INDEX idx_commands_user_id ON commands(user_id);
|
||||
CREATE INDEX idx_connections_custom_user_profile_id ON connections(custom_user_profile_id);
|
||||
CREATE INDEX idx_connections_via_user_contact_link ON connections(via_user_contact_link);
|
||||
CREATE INDEX idx_connections_rcv_file_id ON connections(rcv_file_id);
|
||||
CREATE INDEX idx_connections_contact_id ON connections(contact_id);
|
||||
CREATE INDEX idx_connections_user_contact_link_id ON connections(user_contact_link_id);
|
||||
CREATE INDEX idx_connections_via_contact ON connections(via_contact);
|
||||
CREATE INDEX idx_contact_profiles_user_id ON contact_profiles(user_id);
|
||||
CREATE INDEX idx_contact_requests_contact_profile_id ON contact_requests(contact_profile_id);
|
||||
CREATE INDEX idx_contact_requests_user_contact_link_id ON contact_requests(user_contact_link_id);
|
||||
CREATE INDEX idx_contacts_via_group ON contacts(via_group);
|
||||
CREATE INDEX idx_contacts_contact_profile_id ON contacts(contact_profile_id);
|
||||
CREATE INDEX idx_files_chat_item_id ON files(chat_item_id);
|
||||
CREATE INDEX idx_files_user_id ON files(user_id);
|
||||
CREATE INDEX idx_files_group_id ON files(group_id);
|
||||
CREATE INDEX idx_files_contact_id ON files(contact_id);
|
||||
CREATE INDEX idx_group_member_intros_to_group_member_id ON group_member_intros(to_group_member_id);
|
||||
CREATE INDEX idx_group_members_user_id_local_display_name ON group_members(user_id, local_display_name);
|
||||
CREATE INDEX idx_group_members_member_profile_id ON group_members(member_profile_id);
|
||||
CREATE INDEX idx_group_members_contact_id ON group_members(contact_id);
|
||||
CREATE INDEX idx_group_members_contact_profile_id ON group_members(contact_profile_id);
|
||||
CREATE INDEX idx_group_members_user_id ON group_members(user_id);
|
||||
CREATE INDEX idx_group_members_invited_by ON group_members(invited_by);
|
||||
CREATE INDEX idx_group_profiles_user_id ON group_profiles(user_id);
|
||||
CREATE INDEX idx_groups_host_conn_custom_user_profile_id ON groups(host_conn_custom_user_profile_id);
|
||||
CREATE INDEX idx_groups_chat_item_id ON groups(chat_item_id);
|
||||
CREATE INDEX idx_groups_group_profile_id ON groups(group_profile_id);
|
||||
CREATE INDEX idx_messages_group_id ON messages(group_id);
|
||||
CREATE INDEX idx_pending_group_messages_group_member_intro_id ON pending_group_messages(group_member_intro_id);
|
||||
CREATE INDEX idx_pending_group_messages_message_id ON pending_group_messages(message_id);
|
||||
CREATE INDEX idx_pending_group_messages_group_member_id ON pending_group_messages(group_member_id);
|
||||
CREATE INDEX idx_rcv_file_chunks_file_id ON rcv_file_chunks(file_id);
|
||||
CREATE INDEX idx_rcv_files_group_member_id ON rcv_files(group_member_id);
|
||||
CREATE INDEX idx_received_probes_user_id ON received_probes(user_id);
|
||||
CREATE INDEX idx_received_probes_contact_id ON received_probes(contact_id);
|
||||
CREATE INDEX idx_sent_probe_hashes_user_id ON sent_probe_hashes(user_id);
|
||||
CREATE INDEX idx_sent_probe_hashes_contact_id ON sent_probe_hashes(contact_id);
|
||||
CREATE INDEX idx_settings_user_id ON settings(user_id);
|
||||
CREATE INDEX idx_smp_servers_user_id ON smp_servers(user_id);
|
||||
CREATE INDEX idx_snd_file_chunks_file_id_connection_id ON snd_file_chunks(file_id, connection_id);
|
||||
CREATE INDEX idx_snd_files_group_member_id ON snd_files(group_member_id);
|
||||
CREATE INDEX idx_snd_files_connection_id ON snd_files(connection_id);
|
||||
CREATE INDEX idx_snd_files_file_id ON snd_files(file_id);
|
||||
|]
|
||||
@@ -0,0 +1,39 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20230118_recreate_smp_servers where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
-- UNIQUE constraint includes user_id
|
||||
m20230118_recreate_smp_servers :: Query
|
||||
m20230118_recreate_smp_servers =
|
||||
[sql|
|
||||
DROP INDEX idx_smp_servers_user_id;
|
||||
|
||||
CREATE TABLE new_smp_servers (
|
||||
smp_server_id INTEGER PRIMARY KEY,
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
key_hash BLOB NOT NULL,
|
||||
basic_auth TEXT,
|
||||
preset INTEGER NOT NULL DEFAULT 0,
|
||||
tested INTEGER,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (user_id, host, port)
|
||||
);
|
||||
|
||||
INSERT INTO new_smp_servers
|
||||
(smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
|
||||
SELECT
|
||||
smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at
|
||||
FROM smp_servers;
|
||||
|
||||
DROP TABLE smp_servers;
|
||||
ALTER TABLE new_smp_servers RENAME TO smp_servers;
|
||||
|
||||
CREATE INDEX idx_smp_servers_user_id ON smp_servers(user_id);
|
||||
|]
|
||||
@@ -29,7 +29,8 @@ CREATE TABLE users(
|
||||
local_display_name TEXT NOT NULL UNIQUE,
|
||||
active_user INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT CHECK(created_at NOT NULL),
|
||||
updated_at TEXT CHECK(updated_at NOT NULL), -- 1 for active user
|
||||
updated_at TEXT CHECK(updated_at NOT NULL),
|
||||
agent_user_id INTEGER CHECK(agent_user_id NOT NULL), -- 1 for active user
|
||||
FOREIGN KEY(user_id, local_display_name)
|
||||
REFERENCES display_names(user_id, local_display_name)
|
||||
ON DELETE CASCADE
|
||||
@@ -385,20 +386,6 @@ CREATE INDEX idx_connections_via_contact_uri_hash ON connections(
|
||||
);
|
||||
CREATE INDEX idx_contact_requests_xcontact_id ON contact_requests(xcontact_id);
|
||||
CREATE INDEX idx_contacts_xcontact_id ON contacts(xcontact_id);
|
||||
CREATE TABLE smp_servers(
|
||||
smp_server_id INTEGER PRIMARY KEY,
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
key_hash BLOB NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
basic_auth TEXT,
|
||||
preset INTEGER DEFAULT 0 CHECK(preset NOT NULL),
|
||||
tested INTEGER,
|
||||
enabled INTEGER DEFAULT 1 CHECK(enabled NOT NULL),
|
||||
UNIQUE(host, port)
|
||||
);
|
||||
CREATE INDEX idx_messages_shared_msg_id ON messages(shared_msg_id);
|
||||
CREATE INDEX idx_chat_items_shared_msg_id ON chat_items(shared_msg_id);
|
||||
CREATE TABLE calls(
|
||||
@@ -469,3 +456,94 @@ CREATE INDEX idx_connections_group_member ON connections(
|
||||
group_member_id
|
||||
);
|
||||
CREATE INDEX idx_commands_connection_id ON commands(connection_id);
|
||||
CREATE INDEX idx_calls_user_id ON calls(user_id);
|
||||
CREATE INDEX idx_calls_chat_item_id ON calls(chat_item_id);
|
||||
CREATE INDEX idx_calls_contact_id ON calls(contact_id);
|
||||
CREATE INDEX idx_chat_items_group_id ON chat_items(group_id);
|
||||
CREATE INDEX idx_commands_user_id ON commands(user_id);
|
||||
CREATE INDEX idx_connections_custom_user_profile_id ON connections(
|
||||
custom_user_profile_id
|
||||
);
|
||||
CREATE INDEX idx_connections_via_user_contact_link ON connections(
|
||||
via_user_contact_link
|
||||
);
|
||||
CREATE INDEX idx_connections_rcv_file_id ON connections(rcv_file_id);
|
||||
CREATE INDEX idx_connections_contact_id ON connections(contact_id);
|
||||
CREATE INDEX idx_connections_user_contact_link_id ON connections(
|
||||
user_contact_link_id
|
||||
);
|
||||
CREATE INDEX idx_connections_via_contact ON connections(via_contact);
|
||||
CREATE INDEX idx_contact_profiles_user_id ON contact_profiles(user_id);
|
||||
CREATE INDEX idx_contact_requests_contact_profile_id ON contact_requests(
|
||||
contact_profile_id
|
||||
);
|
||||
CREATE INDEX idx_contact_requests_user_contact_link_id ON contact_requests(
|
||||
user_contact_link_id
|
||||
);
|
||||
CREATE INDEX idx_contacts_via_group ON contacts(via_group);
|
||||
CREATE INDEX idx_contacts_contact_profile_id ON contacts(contact_profile_id);
|
||||
CREATE INDEX idx_files_chat_item_id ON files(chat_item_id);
|
||||
CREATE INDEX idx_files_user_id ON files(user_id);
|
||||
CREATE INDEX idx_files_group_id ON files(group_id);
|
||||
CREATE INDEX idx_files_contact_id ON files(contact_id);
|
||||
CREATE INDEX idx_group_member_intros_to_group_member_id ON group_member_intros(
|
||||
to_group_member_id
|
||||
);
|
||||
CREATE INDEX idx_group_members_user_id_local_display_name ON group_members(
|
||||
user_id,
|
||||
local_display_name
|
||||
);
|
||||
CREATE INDEX idx_group_members_member_profile_id ON group_members(
|
||||
member_profile_id
|
||||
);
|
||||
CREATE INDEX idx_group_members_contact_id ON group_members(contact_id);
|
||||
CREATE INDEX idx_group_members_contact_profile_id ON group_members(
|
||||
contact_profile_id
|
||||
);
|
||||
CREATE INDEX idx_group_members_user_id ON group_members(user_id);
|
||||
CREATE INDEX idx_group_members_invited_by ON group_members(invited_by);
|
||||
CREATE INDEX idx_group_profiles_user_id ON group_profiles(user_id);
|
||||
CREATE INDEX idx_groups_host_conn_custom_user_profile_id ON groups(
|
||||
host_conn_custom_user_profile_id
|
||||
);
|
||||
CREATE INDEX idx_groups_chat_item_id ON groups(chat_item_id);
|
||||
CREATE INDEX idx_groups_group_profile_id ON groups(group_profile_id);
|
||||
CREATE INDEX idx_messages_group_id ON messages(group_id);
|
||||
CREATE INDEX idx_pending_group_messages_group_member_intro_id ON pending_group_messages(
|
||||
group_member_intro_id
|
||||
);
|
||||
CREATE INDEX idx_pending_group_messages_message_id ON pending_group_messages(
|
||||
message_id
|
||||
);
|
||||
CREATE INDEX idx_pending_group_messages_group_member_id ON pending_group_messages(
|
||||
group_member_id
|
||||
);
|
||||
CREATE INDEX idx_rcv_file_chunks_file_id ON rcv_file_chunks(file_id);
|
||||
CREATE INDEX idx_rcv_files_group_member_id ON rcv_files(group_member_id);
|
||||
CREATE INDEX idx_received_probes_user_id ON received_probes(user_id);
|
||||
CREATE INDEX idx_received_probes_contact_id ON received_probes(contact_id);
|
||||
CREATE INDEX idx_sent_probe_hashes_user_id ON sent_probe_hashes(user_id);
|
||||
CREATE INDEX idx_sent_probe_hashes_contact_id ON sent_probe_hashes(contact_id);
|
||||
CREATE INDEX idx_settings_user_id ON settings(user_id);
|
||||
CREATE INDEX idx_snd_file_chunks_file_id_connection_id ON snd_file_chunks(
|
||||
file_id,
|
||||
connection_id
|
||||
);
|
||||
CREATE INDEX idx_snd_files_group_member_id ON snd_files(group_member_id);
|
||||
CREATE INDEX idx_snd_files_connection_id ON snd_files(connection_id);
|
||||
CREATE INDEX idx_snd_files_file_id ON snd_files(file_id);
|
||||
CREATE TABLE IF NOT EXISTS "smp_servers"(
|
||||
smp_server_id INTEGER PRIMARY KEY,
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
key_hash BLOB NOT NULL,
|
||||
basic_auth TEXT,
|
||||
preset INTEGER NOT NULL DEFAULT 0,
|
||||
tested INTEGER,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
UNIQUE(user_id, host, port)
|
||||
);
|
||||
CREATE INDEX idx_smp_servers_user_id ON smp_servers(user_id);
|
||||
|
||||
+182
-130
@@ -25,9 +25,20 @@ module Simplex.Chat.Store
|
||||
createChatStore,
|
||||
chatStoreFile,
|
||||
agentStoreFile,
|
||||
createUser,
|
||||
createUserRecord,
|
||||
getUsersInfo,
|
||||
getUsers,
|
||||
setActiveUser,
|
||||
getSetActiveUser,
|
||||
getUser,
|
||||
getUserIdByName,
|
||||
getUserByAConnId,
|
||||
getUserByContactId,
|
||||
getUserByGroupId,
|
||||
getUserByFileId,
|
||||
getUserByContactRequestId,
|
||||
getUserFileInfo,
|
||||
deleteUserRecord,
|
||||
createDirectConnection,
|
||||
createConnReqConnection,
|
||||
getProfileById,
|
||||
@@ -65,6 +76,7 @@ module Simplex.Chat.Store
|
||||
getGroupLink,
|
||||
getGroupLinkId,
|
||||
createOrUpdateContactRequest,
|
||||
getContactRequest',
|
||||
getContactRequest,
|
||||
getContactRequestIdByName,
|
||||
deleteContactRequest,
|
||||
@@ -154,6 +166,7 @@ module Simplex.Chat.Store
|
||||
deleteSndFileChunks,
|
||||
createRcvFileTransfer,
|
||||
createRcvGroupFileTransfer,
|
||||
getRcvFileTransferById,
|
||||
getRcvFileTransfer,
|
||||
acceptRcvFileTransfer,
|
||||
getContactByFileId,
|
||||
@@ -169,13 +182,9 @@ module Simplex.Chat.Store
|
||||
getFileTransferMeta,
|
||||
getSndFileTransfer,
|
||||
getContactFileInfo,
|
||||
getContactMaxItemTs, -- TODO delete
|
||||
deleteContactCIs,
|
||||
updateContactTs, -- TODO delete
|
||||
getGroupFileInfo,
|
||||
getGroupMaxItemTs, -- TODO delete
|
||||
deleteGroupCIs,
|
||||
updateGroupTs, -- TODO delete
|
||||
createNewSndMessage,
|
||||
createSndMsgDelivery,
|
||||
createNewMessageAndRcvMsgDelivery,
|
||||
@@ -236,10 +245,8 @@ module Simplex.Chat.Store
|
||||
setChatItemTTL,
|
||||
getContactExpiredFileInfo,
|
||||
deleteContactExpiredCIs,
|
||||
getContactCICount, -- TODO delete
|
||||
getGroupExpiredFileInfo,
|
||||
deleteGroupExpiredCIs,
|
||||
getGroupCICount, -- TODO delete
|
||||
getPendingContactConnection,
|
||||
deletePendingContactConnection,
|
||||
updateContactSettings,
|
||||
@@ -327,6 +334,9 @@ import Simplex.Chat.Migrations.M20221222_chat_ts
|
||||
import Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status
|
||||
import Simplex.Chat.Migrations.M20221230_idxs
|
||||
import Simplex.Chat.Migrations.M20230107_connections_auth_err_counter
|
||||
import Simplex.Chat.Migrations.M20230111_users_agent_user_id
|
||||
import Simplex.Chat.Migrations.M20230117_fkey_indexes
|
||||
import Simplex.Chat.Migrations.M20230118_recreate_smp_servers
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (week)
|
||||
@@ -387,7 +397,10 @@ schemaMigrations =
|
||||
("20221222_chat_ts", m20221222_chat_ts),
|
||||
("20221223_idx_chat_items_item_status", m20221223_idx_chat_items_item_status),
|
||||
("20221230_idxs", m20221230_idxs),
|
||||
("20230107_connections_auth_err_counter", m20230107_connections_auth_err_counter)
|
||||
("20230107_connections_auth_err_counter", m20230107_connections_auth_err_counter),
|
||||
("20230111_users_agent_user_id", m20230111_users_agent_user_id),
|
||||
("20230117_fkey_indexes", m20230117_fkey_indexes),
|
||||
("20230118_recreate_smp_servers", m20230118_recreate_smp_servers)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
@@ -416,14 +429,15 @@ handleSQLError err e
|
||||
insertedRowId :: DB.Connection -> IO Int64
|
||||
insertedRowId db = fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()"
|
||||
|
||||
createUser :: DB.Connection -> Profile -> Bool -> ExceptT StoreError IO User
|
||||
createUser db Profile {displayName, fullName, image, preferences = userPreferences} activeUser =
|
||||
createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User
|
||||
createUserRecord db (AgentUserId auId) Profile {displayName, fullName, image, preferences = userPreferences} activeUser =
|
||||
checkConstraint SEDuplicateName . liftIO $ do
|
||||
currentTs <- getCurrentTime
|
||||
when activeUser $ DB.execute_ db "UPDATE users SET active_user = 0"
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO users (local_display_name, active_user, contact_id, created_at, updated_at) VALUES (?,?,0,?,?)"
|
||||
(displayName, activeUser, currentTs, currentTs)
|
||||
"INSERT INTO users (agent_user_id, local_display_name, active_user, contact_id, created_at, updated_at) VALUES (?,?,?,0,?,?)"
|
||||
(auId, displayName, activeUser, currentTs, currentTs)
|
||||
userId <- insertedRowId db
|
||||
DB.execute
|
||||
db
|
||||
@@ -440,30 +454,120 @@ createUser db Profile {displayName, fullName, image, preferences = userPreferenc
|
||||
(profileId, displayName, userId, True, currentTs, currentTs)
|
||||
contactId <- insertedRowId db
|
||||
DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId)
|
||||
pure $ toUser (userId, contactId, profileId, activeUser, displayName, fullName, image, userPreferences)
|
||||
pure $ toUser (userId, auId, contactId, profileId, activeUser, displayName, fullName, image, userPreferences)
|
||||
|
||||
getUsersInfo :: DB.Connection -> IO [UserInfo]
|
||||
getUsersInfo db = getUsers db >>= mapM getUserInfo
|
||||
where
|
||||
getUserInfo :: User -> IO UserInfo
|
||||
getUserInfo user@User {userId} = do
|
||||
ctCount <-
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT COUNT(1)
|
||||
FROM chat_items i
|
||||
JOIN contacts ct USING (contact_id)
|
||||
WHERE i.user_id = ? AND i.item_status = ? AND (ct.enable_ntfs = 1 OR ct.enable_ntfs IS NULL)
|
||||
|]
|
||||
(userId, CISRcvNew)
|
||||
gCount <-
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT COUNT(1)
|
||||
FROM chat_items i
|
||||
JOIN groups g USING (group_id)
|
||||
WHERE i.user_id = ? AND i.item_status = ? AND (g.enable_ntfs = 1 OR g.enable_ntfs IS NULL)
|
||||
|]
|
||||
(userId, CISRcvNew)
|
||||
pure UserInfo {user, unreadCount = fromMaybe 0 ctCount + fromMaybe 0 gCount}
|
||||
|
||||
getUsers :: DB.Connection -> IO [User]
|
||||
getUsers db =
|
||||
map toUser
|
||||
<$> DB.query_
|
||||
db
|
||||
[sql|
|
||||
SELECT u.user_id, u.contact_id, p.contact_profile_id, u.active_user, u.local_display_name, p.full_name, p.image, p.preferences
|
||||
FROM users u
|
||||
JOIN contacts c ON u.contact_id = c.contact_id
|
||||
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
|
||||
|]
|
||||
map toUser <$> DB.query_ db userQuery
|
||||
|
||||
toUser :: (UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe Preferences) -> User
|
||||
toUser (userId, userContactId, profileId, activeUser, displayName, fullName, image, userPreferences) =
|
||||
userQuery :: Query
|
||||
userQuery =
|
||||
[sql|
|
||||
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.local_display_name, ucp.full_name, ucp.image, ucp.preferences
|
||||
FROM users u
|
||||
JOIN contacts uct ON uct.contact_id = u.contact_id
|
||||
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
|
||||
|]
|
||||
|
||||
toUser :: (UserId, UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe Preferences) -> User
|
||||
toUser (userId, auId, userContactId, profileId, activeUser, displayName, fullName, image, userPreferences) =
|
||||
let profile = LocalProfile {profileId, displayName, fullName, image, preferences = userPreferences, localAlias = ""}
|
||||
in User {userId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences = mergePreferences Nothing userPreferences}
|
||||
in User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences = mergePreferences Nothing userPreferences}
|
||||
|
||||
setActiveUser :: DB.Connection -> UserId -> IO ()
|
||||
setActiveUser db userId = do
|
||||
DB.execute_ db "UPDATE users SET active_user = 0"
|
||||
DB.execute db "UPDATE users SET active_user = 1 WHERE user_id = ?" (Only userId)
|
||||
|
||||
getSetActiveUser :: DB.Connection -> UserId -> ExceptT StoreError IO User
|
||||
getSetActiveUser db userId = do
|
||||
liftIO $ setActiveUser db userId
|
||||
getUser db userId
|
||||
|
||||
getUser :: DB.Connection -> UserId -> ExceptT StoreError IO User
|
||||
getUser db userId =
|
||||
ExceptT . firstRow toUser (SEUserNotFound userId) $
|
||||
DB.query db (userQuery <> " WHERE u.user_id = ?") (Only userId)
|
||||
|
||||
getUserIdByName :: DB.Connection -> UserName -> ExceptT StoreError IO Int64
|
||||
getUserIdByName db uName =
|
||||
ExceptT . firstRow fromOnly (SEUserNotFoundByName uName) $
|
||||
DB.query db "SELECT user_id FROM users WHERE local_display_name = ?" (Only uName)
|
||||
|
||||
getUserByAConnId :: DB.Connection -> AgentConnId -> IO (Maybe User)
|
||||
getUserByAConnId db agentConnId =
|
||||
maybeFirstRow toUser $
|
||||
DB.query db (userQuery <> " JOIN connections c ON c.user_id = u.user_id WHERE c.agent_conn_id = ?") (Only agentConnId)
|
||||
|
||||
getUserByContactId :: DB.Connection -> ContactId -> ExceptT StoreError IO User
|
||||
getUserByContactId db contactId =
|
||||
ExceptT . firstRow toUser (SEUserNotFoundByContactId contactId) $
|
||||
DB.query db (userQuery <> " JOIN contacts ct ON ct.user_id = u.user_id WHERE ct.contact_id = ?") (Only contactId)
|
||||
|
||||
getUserByGroupId :: DB.Connection -> GroupId -> ExceptT StoreError IO User
|
||||
getUserByGroupId db groupId =
|
||||
ExceptT . firstRow toUser (SEUserNotFoundByGroupId groupId) $
|
||||
DB.query db (userQuery <> " JOIN groups g ON g.user_id = u.user_id WHERE g.group_id = ?") (Only groupId)
|
||||
|
||||
getUserByFileId :: DB.Connection -> FileTransferId -> ExceptT StoreError IO User
|
||||
getUserByFileId db fileId =
|
||||
ExceptT . firstRow toUser (SEUserNotFoundByFileId fileId) $
|
||||
DB.query db (userQuery <> " JOIN files f ON f.user_id = u.user_id WHERE f.file_id = ?") (Only fileId)
|
||||
|
||||
getUserByContactRequestId :: DB.Connection -> Int64 -> ExceptT StoreError IO User
|
||||
getUserByContactRequestId db contactRequestId =
|
||||
ExceptT . firstRow toUser (SEUserNotFoundByContactRequestId contactRequestId) $
|
||||
DB.query db (userQuery <> " JOIN contact_requests cr ON cr.user_id = u.user_id WHERE cr.contact_request_id = ?") (Only contactRequestId)
|
||||
|
||||
getUserFileInfo :: DB.Connection -> User -> IO [CIFileInfo]
|
||||
getUserFileInfo db User {userId} =
|
||||
map toFileInfo
|
||||
<$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ?") (Only userId)
|
||||
|
||||
fileInfoQuery :: Query
|
||||
fileInfoQuery =
|
||||
[sql|
|
||||
SELECT f.file_id, f.ci_file_status, f.file_path
|
||||
FROM chat_items i
|
||||
JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
|]
|
||||
|
||||
toFileInfo :: (Int64, Maybe ACIFileStatus, Maybe FilePath) -> CIFileInfo
|
||||
toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, filePath}
|
||||
|
||||
deleteUserRecord :: DB.Connection -> User -> IO ()
|
||||
deleteUserRecord db User {userId} =
|
||||
DB.execute db "DELETE FROM users WHERE user_id = ?" (Only userId)
|
||||
|
||||
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> IO PendingContactConnection
|
||||
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId = do
|
||||
createdAt <- getCurrentTime
|
||||
@@ -516,8 +620,8 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
|
||||
"SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1"
|
||||
(userId, cReqHash)
|
||||
|
||||
createDirectConnection :: DB.Connection -> UserId -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> IO PendingContactConnection
|
||||
createDirectConnection db userId acId cReq pccConnStatus incognitoProfile = do
|
||||
createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> IO PendingContactConnection
|
||||
createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile = do
|
||||
createdAt <- getCurrentTime
|
||||
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
|
||||
DB.execute
|
||||
@@ -855,8 +959,8 @@ getUserContactProfiles db User {userId} =
|
||||
toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe Preferences) -> (Profile)
|
||||
toContactProfile (displayName, fullName, image, preferences) = Profile {displayName, fullName, image, preferences}
|
||||
|
||||
createUserContactLink :: DB.Connection -> UserId -> ConnId -> ConnReqContact -> ExceptT StoreError IO ()
|
||||
createUserContactLink db userId agentConnId cReq =
|
||||
createUserContactLink :: DB.Connection -> User -> ConnId -> ConnReqContact -> ExceptT StoreError IO ()
|
||||
createUserContactLink db User {userId} agentConnId cReq =
|
||||
checkConstraint SEDuplicateContactLink . liftIO $ do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
@@ -964,8 +1068,8 @@ toUserContactLink (connReq, autoAccept, acceptIncognito, autoReply) =
|
||||
UserContactLink connReq $
|
||||
if autoAccept then Just AutoAccept {acceptIncognito, autoReply} else Nothing
|
||||
|
||||
getUserAddress :: DB.Connection -> UserId -> ExceptT StoreError IO UserContactLink
|
||||
getUserAddress db userId =
|
||||
getUserAddress :: DB.Connection -> User -> ExceptT StoreError IO UserContactLink
|
||||
getUserAddress db User {userId} =
|
||||
ExceptT . firstRow toUserContactLink SEUserContactLinkNotFound $
|
||||
DB.query
|
||||
db
|
||||
@@ -989,9 +1093,9 @@ getUserContactLinkById db userId userContactLinkId =
|
||||
|]
|
||||
(userId, userContactLinkId)
|
||||
|
||||
updateUserAddressAutoAccept :: DB.Connection -> UserId -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink
|
||||
updateUserAddressAutoAccept db userId autoAccept = do
|
||||
link <- getUserAddress db userId
|
||||
updateUserAddressAutoAccept :: DB.Connection -> User -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink
|
||||
updateUserAddressAutoAccept db user@User {userId} autoAccept = do
|
||||
link <- getUserAddress db user
|
||||
liftIO updateUserAddressAutoAccept_ $> link {autoAccept}
|
||||
where
|
||||
updateUserAddressAutoAccept_ =
|
||||
@@ -1093,10 +1197,10 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
|
||||
createOrUpdate_ = do
|
||||
cReqId <-
|
||||
ExceptT $
|
||||
maybeM getContactRequest' xContactId_ >>= \case
|
||||
maybeM getContactRequestByXContactId xContactId_ >>= \case
|
||||
Nothing -> createContactRequest
|
||||
Just cr -> updateContactRequest cr $> Right (contactRequestId (cr :: UserContactRequest))
|
||||
getContactRequest db userId cReqId
|
||||
getContactRequest db user cReqId
|
||||
createContactRequest :: IO (Either StoreError Int64)
|
||||
createContactRequest = do
|
||||
currentTs <- getCurrentTime
|
||||
@@ -1138,8 +1242,8 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
|
||||
LIMIT 1
|
||||
|]
|
||||
(userId, xContactId)
|
||||
getContactRequest' :: XContactId -> IO (Maybe UserContactRequest)
|
||||
getContactRequest' xContactId =
|
||||
getContactRequestByXContactId :: XContactId -> IO (Maybe UserContactRequest)
|
||||
getContactRequestByXContactId xContactId =
|
||||
maybeFirstRow toContactRequest $
|
||||
DB.query
|
||||
db
|
||||
@@ -1184,8 +1288,13 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
|
||||
|]
|
||||
(displayName, fullName, image, currentTs, userId, cReqId)
|
||||
|
||||
getContactRequest :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO UserContactRequest
|
||||
getContactRequest db userId contactRequestId =
|
||||
getContactRequest' :: DB.Connection -> Int64 -> ExceptT StoreError IO (User, UserContactRequest)
|
||||
getContactRequest' db contactRequestId = do
|
||||
user <- getUserByContactRequestId db contactRequestId
|
||||
(user,) <$> getContactRequest db user contactRequestId
|
||||
|
||||
getContactRequest :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO UserContactRequest
|
||||
getContactRequest db User {userId} contactRequestId =
|
||||
ExceptT . firstRow toContactRequest (SEContactRequestNotFound contactRequestId) $
|
||||
DB.query
|
||||
db
|
||||
@@ -1213,8 +1322,8 @@ getContactRequestIdByName db userId cName =
|
||||
ExceptT . firstRow fromOnly (SEContactRequestNotFoundByName cName) $
|
||||
DB.query db "SELECT contact_request_id FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, cName)
|
||||
|
||||
deleteContactRequest :: DB.Connection -> UserId -> Int64 -> IO ()
|
||||
deleteContactRequest db userId contactRequestId = do
|
||||
deleteContactRequest :: DB.Connection -> User -> Int64 -> IO ()
|
||||
deleteContactRequest db User {userId} contactRequestId = do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
@@ -1635,26 +1744,28 @@ getConnectionById db User {userId} connId = ExceptT $ do
|
||||
|]
|
||||
(userId, connId)
|
||||
|
||||
getConnectionsContacts :: DB.Connection -> UserId -> [ConnId] -> IO [ContactRef]
|
||||
getConnectionsContacts db userId agentConnIds = do
|
||||
getConnectionsContacts :: DB.Connection -> [ConnId] -> IO [ContactRef]
|
||||
getConnectionsContacts db agentConnIds = do
|
||||
DB.execute_ db "DROP TABLE IF EXISTS temp.conn_ids"
|
||||
DB.execute_ db "CREATE TABLE temp.conn_ids (conn_id BLOB)"
|
||||
DB.executeMany db "INSERT INTO temp.conn_ids (conn_id) VALUES (?)" $ map Only agentConnIds
|
||||
conns <-
|
||||
map (uncurry ContactRef)
|
||||
map toContactRef
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT ct.contact_id, ct.local_display_name
|
||||
SELECT ct.contact_id, c.connection_id, c.agent_conn_id, ct.local_display_name
|
||||
FROM contacts ct
|
||||
JOIN connections c ON c.contact_id = ct.contact_id
|
||||
WHERE ct.user_id = ?
|
||||
AND c.agent_conn_id IN (SELECT conn_id FROM temp.conn_ids)
|
||||
WHERE c.agent_conn_id IN (SELECT conn_id FROM temp.conn_ids)
|
||||
AND c.conn_type = ?
|
||||
|]
|
||||
(userId, ConnContact)
|
||||
(Only ConnContact)
|
||||
DB.execute_ db "DROP TABLE temp.conn_ids"
|
||||
pure conns
|
||||
where
|
||||
toContactRef :: (ContactId, Int64, ConnId, ContactName) -> ContactRef
|
||||
toContactRef (contactId, connId, acId, localDisplayName) = ContactRef {contactId, connId, agentConnId = AgentConnId acId, localDisplayName}
|
||||
|
||||
getGroupAndMember :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO (GroupInfo, GroupMember)
|
||||
getGroupAndMember db User {userId, userContactId} groupMemberId =
|
||||
@@ -2742,7 +2853,12 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD
|
||||
(fileId, FSNew, fileConnReq, fileInline, rcvFileInline, groupMemberId, currentTs, currentTs)
|
||||
pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId}
|
||||
|
||||
getRcvFileTransfer :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO RcvFileTransfer
|
||||
getRcvFileTransferById :: DB.Connection -> FileTransferId -> ExceptT StoreError IO (User, RcvFileTransfer)
|
||||
getRcvFileTransferById db fileId = do
|
||||
user <- getUserByFileId db fileId
|
||||
(user,) <$> getRcvFileTransfer db user fileId
|
||||
|
||||
getRcvFileTransfer :: DB.Connection -> User -> FileTransferId -> ExceptT StoreError IO RcvFileTransfer
|
||||
getRcvFileTransfer db User {userId} fileId = do
|
||||
rftRow <-
|
||||
ExceptT . firstRow id (SERcvFileNotFound fileId) $
|
||||
@@ -2963,24 +3079,7 @@ getFileTransferMeta db User {userId} fileId =
|
||||
getContactFileInfo :: DB.Connection -> User -> Contact -> IO [CIFileInfo]
|
||||
getContactFileInfo db User {userId} Contact {contactId} =
|
||||
map toFileInfo
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT f.file_id, f.ci_file_status, f.file_path
|
||||
FROM chat_items i
|
||||
JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
WHERE i.user_id = ? AND i.contact_id = ?
|
||||
|]
|
||||
(userId, contactId)
|
||||
|
||||
toFileInfo :: (Int64, Maybe ACIFileStatus, Maybe FilePath) -> CIFileInfo
|
||||
toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, filePath}
|
||||
|
||||
-- TODO delete
|
||||
getContactMaxItemTs :: DB.Connection -> User -> Contact -> IO (Maybe UTCTime)
|
||||
getContactMaxItemTs db User {userId} Contact {contactId} =
|
||||
fmap join . maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT MAX(item_ts) FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId)
|
||||
<$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ? AND i.contact_id = ?") (userId, contactId)
|
||||
|
||||
deleteContactCIs :: DB.Connection -> User -> Contact -> IO ()
|
||||
deleteContactCIs db user@User {userId} ct@Contact {contactId} = do
|
||||
@@ -2994,46 +3093,16 @@ getContactConnIds_ db User {userId} Contact {contactId} =
|
||||
map fromOnly
|
||||
<$> DB.query db "SELECT connection_id FROM connections WHERE user_id = ? AND contact_id = ?" (userId, contactId)
|
||||
|
||||
-- TODO delete
|
||||
updateContactTs :: DB.Connection -> User -> Contact -> UTCTime -> IO ()
|
||||
updateContactTs db User {userId} Contact {contactId} updatedAt =
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE contacts SET chat_ts = ? WHERE user_id = ? AND contact_id = ?"
|
||||
(updatedAt, userId, contactId)
|
||||
|
||||
getGroupFileInfo :: DB.Connection -> User -> GroupInfo -> IO [CIFileInfo]
|
||||
getGroupFileInfo db User {userId} GroupInfo {groupId} =
|
||||
map toFileInfo
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT f.file_id, f.ci_file_status, f.file_path
|
||||
FROM chat_items i
|
||||
JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
WHERE i.user_id = ? AND i.group_id = ?
|
||||
|]
|
||||
(userId, groupId)
|
||||
|
||||
-- TODO delete
|
||||
getGroupMaxItemTs :: DB.Connection -> User -> GroupInfo -> IO (Maybe UTCTime)
|
||||
getGroupMaxItemTs db User {userId} GroupInfo {groupId} =
|
||||
fmap join . maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT MAX(item_ts) FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId)
|
||||
<$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ? AND i.group_id = ?") (userId, groupId)
|
||||
|
||||
deleteGroupCIs :: DB.Connection -> User -> GroupInfo -> IO ()
|
||||
deleteGroupCIs db User {userId} GroupInfo {groupId} = do
|
||||
DB.execute db "DELETE FROM messages WHERE group_id = ?" (Only groupId)
|
||||
DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId)
|
||||
|
||||
-- TODO delete
|
||||
updateGroupTs :: DB.Connection -> User -> GroupInfo -> UTCTime -> IO ()
|
||||
updateGroupTs db User {userId} GroupInfo {groupId} updatedAt =
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE groups SET chat_ts = ? WHERE user_id = ? AND group_id = ?"
|
||||
(updatedAt, userId, groupId)
|
||||
|
||||
createNewSndMessage :: MsgEncodingI e => DB.Connection -> TVar ChaChaDRG -> ConnOrGroupId -> (SharedMsgId -> NewMessage e) -> ExceptT StoreError IO SndMessage
|
||||
createNewSndMessage db gVar connOrGroupId mkMessage =
|
||||
createWithRandomId gVar $ \sharedMsgId -> do
|
||||
@@ -4502,8 +4571,9 @@ overwriteSMPServers db User {userId} servers =
|
||||
pure $ Right ()
|
||||
|
||||
createCall :: DB.Connection -> User -> Call -> UTCTime -> IO ()
|
||||
createCall db User {userId} Call {contactId, callId, chatItemId, callState} callTs = do
|
||||
createCall db user@User {userId} Call {contactId, callId, chatItemId, callState} callTs = do
|
||||
currentTs <- getCurrentTime
|
||||
deleteCalls db user contactId
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
@@ -4517,19 +4587,17 @@ deleteCalls :: DB.Connection -> User -> ContactId -> IO ()
|
||||
deleteCalls db User {userId} contactId = do
|
||||
DB.execute db "DELETE FROM calls WHERE user_id = ? AND contact_id = ?" (userId, contactId)
|
||||
|
||||
getCalls :: DB.Connection -> User -> IO [Call]
|
||||
getCalls db User {userId} = do
|
||||
getCalls :: DB.Connection -> IO [Call]
|
||||
getCalls db =
|
||||
map toCall
|
||||
<$> DB.query
|
||||
<$> DB.query_
|
||||
db
|
||||
[sql|
|
||||
SELECT
|
||||
contact_id, shared_call_id, chat_item_id, call_state, call_ts
|
||||
FROM calls
|
||||
WHERE user_id = ?
|
||||
ORDER BY call_ts ASC
|
||||
|]
|
||||
(Only userId)
|
||||
where
|
||||
toCall :: (ContactId, CallId, ChatItemId, CallState, UTCTime) -> Call
|
||||
toCall (contactId, callId, chatItemId, callState, callTs) = Call {contactId, callId, chatItemId, callState, callTs}
|
||||
@@ -4704,12 +4772,7 @@ getContactExpiredFileInfo db User {userId} Contact {contactId} expirationDate =
|
||||
map toFileInfo
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT f.file_id, f.ci_file_status, f.file_path
|
||||
FROM chat_items i
|
||||
JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
WHERE i.user_id = ? AND i.contact_id = ? AND i.created_at <= ?
|
||||
|]
|
||||
(fileInfoQuery <> " WHERE i.user_id = ? AND i.contact_id = ? AND i.created_at <= ?")
|
||||
(userId, contactId, expirationDate)
|
||||
|
||||
deleteContactExpiredCIs :: DB.Connection -> User -> Contact -> UTCTime -> IO ()
|
||||
@@ -4719,23 +4782,12 @@ deleteContactExpiredCIs db user@User {userId} ct@Contact {contactId} expirationD
|
||||
DB.execute db "DELETE FROM messages WHERE connection_id = ? AND created_at <= ?" (connId, expirationDate)
|
||||
DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND contact_id = ? AND created_at <= ?" (userId, contactId, expirationDate)
|
||||
|
||||
-- TODO delete
|
||||
getContactCICount :: DB.Connection -> User -> Contact -> IO (Maybe Int64)
|
||||
getContactCICount db User {userId} Contact {contactId} =
|
||||
fmap join . maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT COUNT(1) FROM chat_items WHERE user_id = ? AND contact_id = ?" (userId, contactId)
|
||||
|
||||
getGroupExpiredFileInfo :: DB.Connection -> User -> GroupInfo -> UTCTime -> UTCTime -> IO [CIFileInfo]
|
||||
getGroupExpiredFileInfo db User {userId} GroupInfo {groupId} expirationDate createdAtCutoff =
|
||||
map toFileInfo
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT f.file_id, f.ci_file_status, f.file_path
|
||||
FROM chat_items i
|
||||
JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||
WHERE i.user_id = ? AND i.group_id = ? AND i.item_ts <= ? AND i.created_at <= ?
|
||||
|]
|
||||
(fileInfoQuery <> " WHERE i.user_id = ? AND i.group_id = ? AND i.item_ts <= ? AND i.created_at <= ?")
|
||||
(userId, groupId, expirationDate, createdAtCutoff)
|
||||
|
||||
deleteGroupExpiredCIs :: DB.Connection -> User -> GroupInfo -> UTCTime -> UTCTime -> IO ()
|
||||
@@ -4743,12 +4795,6 @@ deleteGroupExpiredCIs db User {userId} GroupInfo {groupId} expirationDate create
|
||||
DB.execute db "DELETE FROM messages WHERE group_id = ? AND created_at <= ?" (groupId, min expirationDate createdAtCutoff)
|
||||
DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ? AND item_ts <= ? AND created_at <= ?" (userId, groupId, expirationDate, createdAtCutoff)
|
||||
|
||||
-- TODO delete
|
||||
getGroupCICount :: DB.Connection -> User -> GroupInfo -> IO (Maybe Int64)
|
||||
getGroupCICount db User {userId} GroupInfo {groupId} =
|
||||
fmap join . maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT COUNT(1) FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId)
|
||||
|
||||
-- | Saves unique local display name based on passed displayName, suffixed with _N if required.
|
||||
-- This function should be called inside transaction.
|
||||
withLocalDisplayName :: forall a. DB.Connection -> UserId -> Text -> (Text -> IO (Either StoreError a)) -> IO (Either StoreError a)
|
||||
@@ -4812,7 +4858,13 @@ randomBytes gVar = atomically . stateTVar gVar . randomBytesGenerate
|
||||
-- These error type constructors must be added to mobile apps
|
||||
data StoreError
|
||||
= SEDuplicateName
|
||||
| SEContactNotFound {contactId :: Int64}
|
||||
| SEUserNotFound {userId :: UserId}
|
||||
| SEUserNotFoundByName {contactName :: ContactName}
|
||||
| SEUserNotFoundByContactId {contactId :: ContactId}
|
||||
| SEUserNotFoundByGroupId {groupId :: GroupId}
|
||||
| SEUserNotFoundByFileId {fileId :: FileTransferId}
|
||||
| SEUserNotFoundByContactRequestId {contactRequestId :: Int64}
|
||||
| SEContactNotFound {contactId :: ContactId}
|
||||
| SEContactNotFoundByName {contactName :: ContactName}
|
||||
| SEContactNotReady {contactName :: ContactName}
|
||||
| SEDuplicateContactLink
|
||||
|
||||
@@ -17,7 +17,6 @@ import Simplex.Chat.Options
|
||||
import Simplex.Chat.Terminal.Input
|
||||
import Simplex.Chat.Terminal.Notification
|
||||
import Simplex.Chat.Terminal.Output
|
||||
import Simplex.Messaging.Agent.Env.SQLite (InitialAgentServers (..))
|
||||
import Simplex.Messaging.Client (defaultNetworkConfig)
|
||||
import Simplex.Messaging.Util (raceAny_)
|
||||
import System.Exit (exitFailure)
|
||||
@@ -26,7 +25,7 @@ terminalChatConfig :: ChatConfig
|
||||
terminalChatConfig =
|
||||
defaultChatConfig
|
||||
{ defaultServers =
|
||||
InitialAgentServers
|
||||
DefaultAgentServers
|
||||
{ smp =
|
||||
L.fromList
|
||||
[ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion",
|
||||
|
||||
@@ -43,7 +43,8 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
|
||||
unless (isMessage cmd) $ echo s
|
||||
r <- runReaderT (execChatCommand bs) cc
|
||||
case r of
|
||||
CRChatCmdError _ -> when (isMessage cmd) $ echo s
|
||||
CRChatCmdError _ _ -> when (isMessage cmd) $ echo s
|
||||
CRChatError _ _ -> when (isMessage cmd) $ echo s
|
||||
_ -> pure ()
|
||||
printRespToTerminal ct cc False r
|
||||
startLiveMessage cmd r
|
||||
@@ -58,7 +59,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
|
||||
Right SendMessageBroadcast {} -> True
|
||||
_ -> False
|
||||
startLiveMessage :: Either a ChatCommand -> ChatResponse -> IO ()
|
||||
startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItem (AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}})) = do
|
||||
startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItem _ (AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}})) = do
|
||||
whenM (isNothing <$> readTVarIO liveMessageState) $ do
|
||||
let s = T.unpack $ safeDecodeUtf8 msg
|
||||
int = case cType of SCTGroup -> 5000000; _ -> 3000000 :: Int
|
||||
@@ -111,7 +112,7 @@ sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO
|
||||
sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do
|
||||
let bs = encodeUtf8 $ T.pack sentMsg
|
||||
cmd = UpdateLiveMessage chatName chatItemId live bs
|
||||
either CRChatCmdError id <$> runExceptT (processChatCommand cmd) `runReaderT` cc
|
||||
either (CRChatCmdError Nothing) id <$> runExceptT (processChatCommand cmd) `runReaderT` cc
|
||||
|
||||
runTerminalInput :: ChatTerminal -> ChatController -> IO ()
|
||||
runTerminalInput ct cc = withChatTerm ct $ do
|
||||
|
||||
@@ -95,8 +95,8 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems} = do
|
||||
forever $ do
|
||||
(_, r) <- atomically $ readTBQueue outputQ
|
||||
case r of
|
||||
CRNewChatItem ci -> markChatItemRead ci
|
||||
CRChatItemUpdated ci -> markChatItemRead ci
|
||||
CRNewChatItem _ ci -> markChatItemRead ci
|
||||
CRChatItemUpdated _ ci -> markChatItemRead ci
|
||||
_ -> pure ()
|
||||
liveItems <- readTVarIO showLiveItems
|
||||
printRespToTerminal ct cc liveItems r
|
||||
|
||||
@@ -80,8 +80,31 @@ instance IsContact Contact where
|
||||
preferences' Contact {profile = LocalProfile {preferences}} = preferences
|
||||
{-# INLINE preferences' #-}
|
||||
|
||||
newtype AgentUserId = AgentUserId UserId
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance StrEncoding AgentUserId where
|
||||
strEncode (AgentUserId uId) = strEncode uId
|
||||
strDecode s = AgentUserId <$> strDecode s
|
||||
strP = AgentUserId <$> strP
|
||||
|
||||
instance FromJSON AgentUserId where
|
||||
parseJSON = strParseJSON "AgentUserId"
|
||||
|
||||
instance ToJSON AgentUserId where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
instance FromField AgentUserId where fromField f = AgentUserId <$> fromField f
|
||||
|
||||
instance ToField AgentUserId where toField (AgentUserId uId) = toField uId
|
||||
|
||||
aUserId :: User -> UserId
|
||||
aUserId User {agentUserId = AgentUserId uId} = uId
|
||||
|
||||
data User = User
|
||||
{ userId :: UserId,
|
||||
agentUserId :: AgentUserId,
|
||||
userContactId :: ContactId,
|
||||
localDisplayName :: ContactName,
|
||||
profile :: LocalProfile,
|
||||
@@ -92,7 +115,17 @@ data User = User
|
||||
|
||||
instance ToJSON User where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
type UserId = ContactId
|
||||
data UserInfo = UserInfo
|
||||
{ user :: User,
|
||||
unreadCount :: Int
|
||||
}
|
||||
deriving (Show, Generic, FromJSON)
|
||||
|
||||
instance ToJSON UserInfo where
|
||||
toJSON = J.genericToJSON J.defaultOptions
|
||||
toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
type UserId = Int64
|
||||
|
||||
type ContactId = Int64
|
||||
|
||||
@@ -139,6 +172,8 @@ contactSecurityCode Contact {activeConn} = connectionCode activeConn
|
||||
|
||||
data ContactRef = ContactRef
|
||||
{ contactId :: ContactId,
|
||||
connId :: Int64,
|
||||
agentConnId :: AgentConnId,
|
||||
localDisplayName :: ContactName
|
||||
}
|
||||
deriving (Eq, Show, Generic)
|
||||
@@ -216,6 +251,8 @@ instance ToJSON ConnReqUriHash where
|
||||
|
||||
data ContactOrRequest = CORContact Contact | CORRequest UserContactRequest
|
||||
|
||||
type UserName = Text
|
||||
|
||||
type ContactName = Text
|
||||
|
||||
type GroupName = Text
|
||||
@@ -1828,7 +1865,7 @@ data CommandFunction
|
||||
| CFAllowConn
|
||||
| CFAcceptContact
|
||||
| CFAckMessage
|
||||
| CFDeleteConn
|
||||
| CFDeleteConn -- not used
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance FromField CommandFunction where fromField = fromTextField_ textDecode
|
||||
|
||||
+152
-119
@@ -59,36 +59,37 @@ serializeChatResponse user_ ts = unlines . map unStyle . responseToView user_ de
|
||||
responseToView :: Maybe User -> ChatConfig -> Bool -> CurrentTime -> ChatResponse -> [StyledString]
|
||||
responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
|
||||
CRActiveUser User {profile} -> viewUserProfile $ fromLocalProfile profile
|
||||
CRUsersList users -> viewUsersList users
|
||||
CRChatStarted -> ["chat started"]
|
||||
CRChatRunning -> ["chat is running"]
|
||||
CRChatStopped -> ["chat stopped"]
|
||||
CRChatSuspended -> ["chat suspended"]
|
||||
CRApiChats chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats]
|
||||
CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [plain . bshow $ J.encode chats]
|
||||
CRChats chats -> viewChats ts chats
|
||||
CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat]
|
||||
CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [plain . bshow $ J.encode chat]
|
||||
CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft]
|
||||
CRUserSMPServers smpServers _ -> viewSMPServers (L.toList smpServers) testView
|
||||
CRSmpTestResult testFailure -> viewSMPTestResult testFailure
|
||||
CRChatItemTTL ttl -> viewChatItemTTL ttl
|
||||
CRUserSMPServers u smpServers _ -> ttyUser u $ viewSMPServers (L.toList smpServers) testView
|
||||
CRSmpTestResult u testFailure -> ttyUser u $ viewSMPTestResult testFailure
|
||||
CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl
|
||||
CRNetworkConfig cfg -> viewNetworkConfig cfg
|
||||
CRContactInfo ct cStats customUserProfile -> viewContactInfo ct cStats customUserProfile
|
||||
CRGroupMemberInfo g m cStats -> viewGroupMemberInfo g m cStats
|
||||
CRContactSwitch ct progress -> viewContactSwitch ct progress
|
||||
CRGroupMemberSwitch g m progress -> viewGroupMemberSwitch g m progress
|
||||
CRConnectionVerified verified code -> [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
|
||||
CRContactCode ct code -> viewContactCode ct code testView
|
||||
CRGroupMemberCode g m code -> viewGroupMemberCode g m code testView
|
||||
CRNewChatItem (AChatItem _ _ chat item) -> unmuted chat item $ viewChatItem chat item False ts
|
||||
CRChatItems chatItems -> concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts) chatItems
|
||||
CRChatItemId itemId -> [plain $ maybe "no item" show itemId]
|
||||
CRChatItemStatusUpdated _ -> []
|
||||
CRChatItemUpdated (AChatItem _ _ chat item) -> unmuted chat item $ viewItemUpdate chat item liveItems ts
|
||||
CRChatItemDeleted (AChatItem _ _ chat deletedItem) toItem byUser timed -> unmuted chat deletedItem $ viewItemDelete chat deletedItem (isJust toItem) byUser timed ts
|
||||
CRChatItemDeletedNotFound Contact {localDisplayName = c} _ -> [ttyFrom $ c <> "> [deleted - original message not found]"]
|
||||
CRBroadcastSent mc n t -> viewSentBroadcast mc n ts t
|
||||
CRMsgIntegrityError mErr -> viewMsgIntegrityError mErr
|
||||
CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile
|
||||
CRGroupMemberInfo u g m cStats -> ttyUser u $ viewGroupMemberInfo g m cStats
|
||||
CRContactSwitch u ct progress -> ttyUser u $ viewContactSwitch ct progress
|
||||
CRGroupMemberSwitch u g m progress -> ttyUser u $ viewGroupMemberSwitch g m progress
|
||||
CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
|
||||
CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView
|
||||
CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView
|
||||
CRNewChatItem u (AChatItem _ _ chat item) -> ttyUser u $ unmuted chat item $ viewChatItem chat item False ts
|
||||
CRChatItems u chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts) chatItems
|
||||
CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId]
|
||||
CRChatItemStatusUpdated u _ -> ttyUser u []
|
||||
CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted chat item $ viewItemUpdate chat item liveItems ts
|
||||
CRChatItemDeleted u (AChatItem _ _ chat deletedItem) toItem byUser timed -> ttyUser u $ unmuted chat deletedItem $ viewItemDelete chat deletedItem (isJust toItem) byUser timed ts
|
||||
CRChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"]
|
||||
CRBroadcastSent u mc n t -> ttyUser u $ viewSentBroadcast mc n ts t
|
||||
CRMsgIntegrityError u mErr -> ttyUser u $ viewMsgIntegrityError mErr
|
||||
CRCmdAccepted _ -> []
|
||||
CRCmdOk -> ["ok"]
|
||||
CRCmdOk u_ -> ttyUser' u_ ["ok"]
|
||||
CRChatHelp section -> case section of
|
||||
HSMain -> chatHelpInfo
|
||||
HSFiles -> filesHelpInfo
|
||||
@@ -98,117 +99,114 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
|
||||
HSMarkdown -> markdownInfo
|
||||
HSSettings -> settingsInfo
|
||||
CRWelcome user -> chatWelcome user
|
||||
CRContactsList cs -> viewContactsList cs
|
||||
CRUserContactLink UserContactLink {connReqContact, autoAccept} -> connReqContact_ "Your chat address:" connReqContact <> autoAcceptStatus_ autoAccept
|
||||
CRUserContactLinkUpdated UserContactLink {autoAccept} -> autoAcceptStatus_ autoAccept
|
||||
CRContactRequestRejected UserContactRequest {localDisplayName = c} -> [ttyContact c <> ": contact request rejected"]
|
||||
CRGroupCreated g -> viewGroupCreated g
|
||||
CRGroupMembers g -> viewGroupMembers g
|
||||
CRGroupsList gs -> viewGroupsList gs
|
||||
CRSentGroupInvitation g c _ ->
|
||||
if viaGroupLink . contactConn $ c
|
||||
then [ttyContact' c <> " invited to group " <> ttyGroup' g <> " via your group link"]
|
||||
else ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c]
|
||||
CRFileTransferStatus ftStatus -> viewFileTransferStatus ftStatus
|
||||
CRUserProfile p -> viewUserProfile p
|
||||
CRUserProfileNoChange -> ["user profile did not change"]
|
||||
CRContactsList u cs -> ttyUser u $ viewContactsList cs
|
||||
CRUserContactLink u UserContactLink {connReqContact, autoAccept} -> ttyUser u $ connReqContact_ "Your chat address:" connReqContact <> autoAcceptStatus_ autoAccept
|
||||
CRUserContactLinkUpdated u UserContactLink {autoAccept} -> ttyUser u $ autoAcceptStatus_ autoAccept
|
||||
CRContactRequestRejected u UserContactRequest {localDisplayName = c} -> ttyUser u [ttyContact c <> ": contact request rejected"]
|
||||
CRGroupCreated u g -> ttyUser u $ viewGroupCreated g
|
||||
CRGroupMembers u g -> ttyUser u $ viewGroupMembers g
|
||||
CRGroupsList u gs -> ttyUser u $ viewGroupsList gs
|
||||
CRSentGroupInvitation u g c _ ->
|
||||
ttyUser u $
|
||||
if viaGroupLink . contactConn $ c
|
||||
then [ttyContact' c <> " invited to group " <> ttyGroup' g <> " via your group link"]
|
||||
else ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c]
|
||||
CRFileTransferStatus u ftStatus -> ttyUser u $ viewFileTransferStatus ftStatus
|
||||
CRUserProfile u p -> ttyUser u $ viewUserProfile p
|
||||
CRUserProfileNoChange u -> ttyUser u ["user profile did not change"]
|
||||
CRVersionInfo info -> viewVersionInfo logLevel info
|
||||
CRChatCmdError e -> viewChatError logLevel e
|
||||
CRInvitation cReq -> viewConnReqInvitation cReq
|
||||
CRSentConfirmation -> ["confirmation sent!"]
|
||||
CRSentInvitation customUserProfile -> viewSentInvitation customUserProfile testView
|
||||
CRContactDeleted c -> [ttyContact' c <> ": contact is deleted"]
|
||||
CRChatCleared chatInfo -> viewChatCleared chatInfo
|
||||
CRAcceptingContactRequest c -> [ttyFullContact c <> ": accepting contact request..."]
|
||||
CRContactAlreadyExists c -> [ttyFullContact c <> ": contact already exists"]
|
||||
CRContactRequestAlreadyAccepted c -> [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"]
|
||||
CRUserContactLinkCreated cReq -> connReqContact_ "Your new chat address is created!" cReq
|
||||
CRUserContactLinkDeleted -> viewUserContactLinkDeleted
|
||||
CRUserAcceptedGroupSent _g _ -> [] -- [ttyGroup' g <> ": joining the group..."]
|
||||
CRUserDeletedMember g m -> [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"]
|
||||
CRLeftMemberUser g -> [ttyGroup' g <> ": you left the group"] <> groupPreserved g
|
||||
CRGroupDeletedUser g -> [ttyGroup' g <> ": you deleted the group"]
|
||||
CRRcvFileAccepted ci -> savingFile' ci
|
||||
CRRcvFileAcceptedSndCancelled ft -> viewRcvFileSndCancelled ft
|
||||
CRSndGroupFileCancelled _ ftm fts -> viewSndGroupFileCancelled ftm fts
|
||||
CRRcvFileCancelled ft -> receivingFile_ "cancelled" ft
|
||||
CRUserProfileUpdated p p' -> viewUserProfileUpdated p p'
|
||||
CRContactPrefsUpdated {fromContact, toContact} -> case user_ of
|
||||
Just user -> viewUserContactPrefsUpdated user fromContact toContact
|
||||
_ -> ["unexpected chat event CRContactPrefsUpdated without current user"]
|
||||
CRContactAliasUpdated c -> viewContactAliasUpdated c
|
||||
CRConnectionAliasUpdated c -> viewConnectionAliasUpdated c
|
||||
CRContactUpdated {fromContact = c, toContact = c'} -> case user_ of
|
||||
Just user -> viewContactUpdated c c' <> viewContactPrefsUpdated user c c'
|
||||
_ -> ["unexpected chat event CRContactUpdated without current user"]
|
||||
CRContactsMerged intoCt mergedCt -> viewContactsMerged intoCt mergedCt
|
||||
CRReceivedContactRequest UserContactRequest {localDisplayName = c, profile} -> viewReceivedContactRequest c profile
|
||||
CRRcvFileStart ci -> receivingFile_' "started" ci
|
||||
CRRcvFileComplete ci -> receivingFile_' "completed" ci
|
||||
CRRcvFileSndCancelled ft -> viewRcvFileSndCancelled ft
|
||||
CRSndFileStart _ ft -> sendingFile_ "started" ft
|
||||
CRSndFileComplete _ ft -> sendingFile_ "completed" ft
|
||||
CRInvitation u cReq -> ttyUser u $ viewConnReqInvitation cReq
|
||||
CRSentConfirmation u -> ttyUser u ["confirmation sent!"]
|
||||
CRSentInvitation u customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
|
||||
CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"]
|
||||
CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo
|
||||
CRAcceptingContactRequest u c -> ttyUser u [ttyFullContact c <> ": accepting contact request..."]
|
||||
CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"]
|
||||
CRContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"]
|
||||
CRUserContactLinkCreated u cReq -> ttyUser u $ connReqContact_ "Your new chat address is created!" cReq
|
||||
CRUserContactLinkDeleted u -> ttyUser u viewUserContactLinkDeleted
|
||||
CRUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."]
|
||||
CRUserDeletedMember u g m -> ttyUser u [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"]
|
||||
CRLeftMemberUser u g -> ttyUser u $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g
|
||||
CRGroupDeletedUser u g -> ttyUser u [ttyGroup' g <> ": you deleted the group"]
|
||||
CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci
|
||||
CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
|
||||
CRSndGroupFileCancelled u _ ftm fts -> ttyUser u $ viewSndGroupFileCancelled ftm fts
|
||||
CRRcvFileCancelled u ft -> ttyUser u $ receivingFile_ "cancelled" ft
|
||||
CRUserProfileUpdated u p p' -> ttyUser u $ viewUserProfileUpdated p p'
|
||||
CRContactPrefsUpdated {user = u, fromContact, toContact} -> ttyUser u $ viewUserContactPrefsUpdated u fromContact toContact
|
||||
CRContactAliasUpdated u c -> ttyUser u $ viewContactAliasUpdated c
|
||||
CRConnectionAliasUpdated u c -> ttyUser u $ viewConnectionAliasUpdated c
|
||||
CRContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c'
|
||||
CRContactsMerged u intoCt mergedCt -> ttyUser u $ viewContactsMerged intoCt mergedCt
|
||||
CRReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile
|
||||
CRRcvFileStart u ci -> ttyUser u $ receivingFile_' "started" ci
|
||||
CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' "completed" ci
|
||||
CRRcvFileSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
|
||||
CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft
|
||||
CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft
|
||||
CRSndFileCancelled _ ft -> sendingFile_ "cancelled" ft
|
||||
CRSndFileRcvCancelled _ ft@SndFileTransfer {recipientDisplayName = c} ->
|
||||
[ttyContact c <> " cancelled receiving " <> sndFile ft]
|
||||
CRContactConnecting _ -> []
|
||||
CRContactConnected ct userCustomProfile -> viewContactConnected ct userCustomProfile testView
|
||||
CRContactAnotherClient c -> [ttyContact' c <> ": contact is connected to another client"]
|
||||
CRSubscriptionEnd acEntity -> [sShow (connId (entityConnection acEntity :: Connection)) <> ": END"]
|
||||
CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} ->
|
||||
ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft]
|
||||
CRContactConnecting u _ -> ttyUser u []
|
||||
CRContactConnected u ct userCustomProfile -> ttyUser u $ viewContactConnected ct userCustomProfile testView
|
||||
CRContactAnotherClient u c -> ttyUser u [ttyContact' c <> ": contact is connected to another client"]
|
||||
CRSubscriptionEnd u acEntity -> ttyUser u [sShow (connId (entityConnection acEntity :: Connection)) <> ": END"]
|
||||
CRContactsDisconnected srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"]
|
||||
CRContactsSubscribed srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"]
|
||||
CRContactSubError c e -> [ttyContact' c <> ": contact error " <> sShow e]
|
||||
CRContactSubSummary summary ->
|
||||
[sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors"
|
||||
CRContactSubSummary u summary ->
|
||||
ttyUser u $ [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors"
|
||||
where
|
||||
(errors, subscribed) = partition (isJust . contactError) summary
|
||||
CRUserContactSubSummary summary ->
|
||||
map addressSS addresses
|
||||
<> ([sShow (length groupLinksSubscribed) <> " group links active" | not (null groupLinksSubscribed)] <> viewErrorsSummary groupLinkErrors " group link errors")
|
||||
CRUserContactSubSummary u summary ->
|
||||
ttyUser u $
|
||||
map addressSS addresses
|
||||
<> ([sShow (length groupLinksSubscribed) <> " group links active" | not (null groupLinksSubscribed)] <> viewErrorsSummary groupLinkErrors " group link errors")
|
||||
where
|
||||
(addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary
|
||||
addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError
|
||||
(groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks
|
||||
CRGroupInvitation g -> [groupInvitation' g]
|
||||
CRReceivedGroupInvitation g c role -> viewReceivedGroupInvitation g c role
|
||||
CRUserJoinedGroup g _ -> viewUserJoinedGroup g
|
||||
CRJoinedGroupMember g m -> viewJoinedGroupMember g m
|
||||
CRGroupInvitation u g -> ttyUser u [groupInvitation' g]
|
||||
CRReceivedGroupInvitation u g c role -> ttyUser u $ viewReceivedGroupInvitation g c role
|
||||
CRUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g
|
||||
CRJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m
|
||||
CRHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h]
|
||||
CRHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h]
|
||||
CRJoinedGroupMemberConnecting g host m -> [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"]
|
||||
CRConnectedToGroupMember g m -> [ttyGroup' g <> ": " <> connectedMember m <> " is connected"]
|
||||
CRMemberRole g by m r r' -> viewMemberRoleChanged g by m r r'
|
||||
CRMemberRoleUser g m r r' -> viewMemberRoleUserChanged g m r r'
|
||||
CRDeletedMemberUser g by -> [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group"] <> groupPreserved g
|
||||
CRDeletedMember g by m -> [ttyGroup' g <> ": " <> ttyMember by <> " removed " <> ttyMember m <> " from the group"]
|
||||
CRLeftMember g m -> [ttyGroup' g <> ": " <> ttyMember m <> " left the group"]
|
||||
CRGroupEmpty g -> [ttyFullGroup g <> ": group is empty"]
|
||||
CRGroupRemoved g -> [ttyFullGroup g <> ": you are no longer a member or group deleted"]
|
||||
CRGroupDeleted g m -> [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> groupName' g) <> " to delete the local copy of the group"]
|
||||
CRGroupUpdated g g' m -> viewGroupUpdated g g' m
|
||||
CRGroupProfile g -> viewGroupProfile g
|
||||
CRGroupLinkCreated g cReq -> groupLink_ "Group link is created!" g cReq
|
||||
CRGroupLink g cReq -> groupLink_ "Group link:" g cReq
|
||||
CRGroupLinkDeleted g -> viewGroupLinkDeleted g
|
||||
CRAcceptingGroupJoinRequest g c -> [ttyFullContact c <> ": accepting request to join group " <> ttyGroup' g <> "..."]
|
||||
CRMemberSubError g m e -> [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e]
|
||||
CRMemberSubSummary summary -> viewErrorsSummary (filter (isJust . memberError) summary) " group member errors"
|
||||
CRGroupSubscribed g -> viewGroupSubscribed g
|
||||
CRPendingSubSummary _ -> []
|
||||
CRSndFileSubError SndFileTransfer {fileId, fileName} e ->
|
||||
["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e]
|
||||
CRRcvFileSubError RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e ->
|
||||
["received file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e]
|
||||
CRCallInvitation RcvCallInvitation {contact, callType, sharedKey} -> viewCallInvitation contact callType sharedKey
|
||||
CRCallOffer {contact, callType, offer, sharedKey} -> viewCallOffer contact callType offer sharedKey
|
||||
CRCallAnswer {contact, answer} -> viewCallAnswer contact answer
|
||||
CRCallExtraInfo {contact} -> ["call extra info from " <> ttyContact' contact]
|
||||
CRCallEnded {contact} -> ["call with " <> ttyContact' contact <> " ended"]
|
||||
CRJoinedGroupMemberConnecting u g host m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"]
|
||||
CRConnectedToGroupMember u g m -> ttyUser u [ttyGroup' g <> ": " <> connectedMember m <> " is connected"]
|
||||
CRMemberRole u g by m r r' -> ttyUser u $ viewMemberRoleChanged g by m r r'
|
||||
CRMemberRoleUser u g m r r' -> ttyUser u $ viewMemberRoleUserChanged g m r r'
|
||||
CRDeletedMemberUser u g by -> ttyUser u $ [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group"] <> groupPreserved g
|
||||
CRDeletedMember u g by m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember by <> " removed " <> ttyMember m <> " from the group"]
|
||||
CRLeftMember u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " left the group"]
|
||||
CRGroupEmpty u g -> ttyUser u [ttyFullGroup g <> ": group is empty"]
|
||||
CRGroupRemoved u g -> ttyUser u [ttyFullGroup g <> ": you are no longer a member or group deleted"]
|
||||
CRGroupDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> groupName' g) <> " to delete the local copy of the group"]
|
||||
CRGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m
|
||||
CRGroupProfile u g -> ttyUser u $ viewGroupProfile g
|
||||
CRGroupLinkCreated u g cReq -> ttyUser u $ groupLink_ "Group link is created!" g cReq
|
||||
CRGroupLink u g cReq -> ttyUser u $ groupLink_ "Group link:" g cReq
|
||||
CRGroupLinkDeleted u g -> ttyUser u $ viewGroupLinkDeleted g
|
||||
CRAcceptingGroupJoinRequest _ g c -> [ttyFullContact c <> ": accepting request to join group " <> ttyGroup' g <> "..."]
|
||||
CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e]
|
||||
CRMemberSubSummary u summary -> ttyUser u $ viewErrorsSummary (filter (isJust . memberError) summary) " group member errors"
|
||||
CRGroupSubscribed u g -> ttyUser u $ viewGroupSubscribed g
|
||||
CRPendingSubSummary u _ -> ttyUser u []
|
||||
CRSndFileSubError u SndFileTransfer {fileId, fileName} e ->
|
||||
ttyUser u ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e]
|
||||
CRRcvFileSubError u RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e ->
|
||||
ttyUser u ["received file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e]
|
||||
CRCallInvitation RcvCallInvitation {user, contact, callType, sharedKey} -> ttyUser user $ viewCallInvitation contact callType sharedKey
|
||||
CRCallOffer {user = u, contact, callType, offer, sharedKey} -> ttyUser u $ viewCallOffer contact callType offer sharedKey
|
||||
CRCallAnswer {user = u, contact, answer} -> ttyUser u $ viewCallAnswer contact answer
|
||||
CRCallExtraInfo {user = u, contact} -> ttyUser u ["call extra info from " <> ttyContact' contact]
|
||||
CRCallEnded {user = u, contact} -> ttyUser u ["call with " <> ttyContact' contact <> " ended"]
|
||||
CRCallInvitations _ -> []
|
||||
CRUserContactLinkSubscribed -> ["Your address is active! To show: " <> highlight' "/sa"]
|
||||
CRUserContactLinkSubError e -> ["user address error: " <> sShow e, "to delete your address: " <> highlight' "/da"]
|
||||
CRNewContactConnection _ -> []
|
||||
CRContactConnectionDeleted PendingContactConnection {pccConnId} -> ["connection :" <> sShow pccConnId <> " deleted"]
|
||||
CRNewContactConnection u _ -> ttyUser u []
|
||||
CRContactConnectionDeleted u PendingContactConnection {pccConnId} -> ttyUser u ["connection :" <> sShow pccConnId <> " deleted"]
|
||||
CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)]
|
||||
CRNtfToken _ status mode -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode)]
|
||||
CRNtfMessages {} -> []
|
||||
@@ -219,9 +217,29 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
|
||||
]
|
||||
CRAgentStats stats -> map (plain . intercalate ",") stats
|
||||
CRConnectionDisabled entity -> viewConnectionEntityDisabled entity
|
||||
CRMessageError prefix err -> [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning]
|
||||
CRChatError e -> viewChatError logLevel e
|
||||
CRAgentRcvQueueDeleted acId srv aqId err_ ->
|
||||
[ "completed deleting rcv queue, agent connection id: " <> sShow acId
|
||||
<> (", server: " <> sShow srv)
|
||||
<> (", agent queue id: " <> sShow aqId)
|
||||
<> maybe "" (\e -> ", error: " <> sShow e) err_
|
||||
| logLevel <= CLLInfo
|
||||
]
|
||||
CRAgentConnDeleted acId -> ["completed deleting connection, agent connection id: " <> sShow acId | logLevel <= CLLInfo]
|
||||
CRAgentUserDeleted auId -> ["completed deleting user" <> if logLevel <= CLLInfo then ", agent user id: " <> sShow auId else ""]
|
||||
CRMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning]
|
||||
CRChatCmdError u e -> ttyUser' u $ viewChatError logLevel e
|
||||
CRChatError u e -> ttyUser' u $ viewChatError logLevel e
|
||||
where
|
||||
ttyUser :: User -> [StyledString] -> [StyledString]
|
||||
ttyUser _ [] = []
|
||||
ttyUser User {userId, localDisplayName = u} ss = prependFirst userPrefix ss
|
||||
where
|
||||
userPrefix = case user_ of
|
||||
Just User {userId = activeUserId} -> if userId /= activeUserId then prefix else ""
|
||||
_ -> prefix
|
||||
prefix = "[user: " <> highlight u <> "] "
|
||||
ttyUser' :: Maybe User -> [StyledString] -> [StyledString]
|
||||
ttyUser' = maybe id ttyUser
|
||||
testViewChats :: [AChat] -> [StyledString]
|
||||
testViewChats chats = [sShow $ map toChatView chats]
|
||||
where
|
||||
@@ -258,6 +276,16 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
|
||||
| muted chat chatItem = []
|
||||
| otherwise = s
|
||||
|
||||
viewUsersList :: [UserInfo] -> [StyledString]
|
||||
viewUsersList = map userInfo . sortOn ldn
|
||||
where
|
||||
ldn (UserInfo User {localDisplayName = n} _) = T.toLower n
|
||||
userInfo (UserInfo User {localDisplayName = n, profile = LocalProfile {fullName}, activeUser} count) =
|
||||
ttyFullName n fullName <> active <> unread
|
||||
where
|
||||
active = if activeUser then highlight' " (active)" else ""
|
||||
unread = if count /= 0 then plain $ " (unread: " <> show count <> ")" else ""
|
||||
|
||||
muted :: ChatInfo c -> ChatItem c d -> Bool
|
||||
muted chat ChatItem {chatDir} = case (chat, chatDir) of
|
||||
(DirectChat Contact {chatSettings = DisableNtfs}, CIDirectRcv) -> True
|
||||
@@ -1155,7 +1183,11 @@ viewChatError :: ChatLogLevel -> ChatError -> [StyledString]
|
||||
viewChatError logLevel = \case
|
||||
ChatError err -> case err of
|
||||
CENoActiveUser -> ["error: active user is required"]
|
||||
CENoConnectionUser agentConnId -> ["error: message user not found, conn id: " <> sShow agentConnId | logLevel <= CLLError]
|
||||
CEActiveUserExists -> ["error: active user already exists"]
|
||||
CEDifferentActiveUser commandUserId activeUserId -> ["error: different active user, command user id: " <> sShow commandUserId <> ", active user id: " <> sShow activeUserId]
|
||||
CECantDeleteActiveUser _ -> ["cannot delete active user"]
|
||||
CECantDeleteLastUser _ -> ["cannot delete last user"]
|
||||
CEChatNotStarted -> ["error: chat not started"]
|
||||
CEChatNotStopped -> ["error: chat not stopped"]
|
||||
CEChatStoreChanged -> ["error: chat store changed, please restart chat"]
|
||||
@@ -1206,6 +1238,7 @@ viewChatError logLevel = \case
|
||||
-- e -> ["chat error: " <> sShow e]
|
||||
ChatErrorStore err -> case err of
|
||||
SEDuplicateName -> ["this display name is already used by user, contact or group"]
|
||||
SEUserNotFoundByName u -> ["no user " <> ttyContact u]
|
||||
SEContactNotFoundByName c -> ["no contact " <> ttyContact c]
|
||||
SEContactNotReady c -> ["contact " <> ttyContact c <> " is not active yet"]
|
||||
SEGroupNotFoundByName g -> ["no group " <> ttyGroup g]
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ extra-deps:
|
||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||
# - ../simplexmq
|
||||
- github: simplex-chat/simplexmq
|
||||
commit: 56eea29ec36ba723fda6ae27a18e03a4e5aad6e8
|
||||
commit: b59669a65eafeb92d4a986a03e481ecb343ee307
|
||||
# - ../direct-sqlcipher
|
||||
- github: simplex-chat/direct-sqlcipher
|
||||
commit: 34309410eb2069b029b8fc1872deb1e0db123294
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ import Simplex.Chat.Options
|
||||
import Simplex.Chat.Store
|
||||
import Simplex.Chat.Terminal
|
||||
import Simplex.Chat.Terminal.Output (newChatTerminal)
|
||||
import Simplex.Chat.Types (Profile, User (..))
|
||||
import Simplex.Chat.Types (AgentUserId (..), Profile, User (..))
|
||||
import Simplex.Messaging.Agent.Env.SQLite
|
||||
import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Client (ProtocolClientConfig (..), defaultNetworkConfig)
|
||||
@@ -110,7 +110,7 @@ testCfgV1 = testCfg {agentConfig = testAgentCfgV1}
|
||||
createTestChat :: ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC
|
||||
createTestChat cfg opts@ChatOpts {dbKey} dbPrefix profile = do
|
||||
db@ChatDatabase {chatStore} <- createChatDatabase (testDBPrefix <> dbPrefix) dbKey False
|
||||
Right user <- withTransaction chatStore $ \db' -> runExceptT $ createUser db' profile True
|
||||
Right user <- withTransaction chatStore $ \db' -> runExceptT $ createUserRecord db' (AgentUserId 1) profile True
|
||||
startTestChat_ db cfg opts user
|
||||
|
||||
startTestChat :: ChatConfig -> ChatOpts -> String -> IO TestCC
|
||||
|
||||
+724
-23
@@ -175,6 +175,18 @@ chatTests = do
|
||||
describe "mute/unmute messages" $ do
|
||||
it "mute/unmute contact" testMuteContact
|
||||
it "mute/unmute group" testMuteGroup
|
||||
describe "multiple users" $ do
|
||||
it "create second user" testCreateSecondUser
|
||||
it "multiple users subscribe and receive messages after restart" testUsersSubscribeAfterRestart
|
||||
it "both users have contact link" testMultipleUserAddresses
|
||||
it "create user with default servers" testCreateUserDefaultServers
|
||||
it "create user with same servers" testCreateUserSameServers
|
||||
it "delete user" testDeleteUser
|
||||
it "users have different chat item TTL configuration, chat items expire" testUsersDifferentCIExpirationTTL
|
||||
it "chat items expire after restart for all users according to per user configuration" testUsersRestartCIExpiration
|
||||
it "chat items only expire for users who configured expiration" testEnableCIExpirationOnlyForOneUser
|
||||
it "disabling chat item expiration doesn't disable it for other users" testDisableCIExpirationOnlyForOneUser
|
||||
it "both users have configured timed messages with contacts, messages expire, restart" testUsersTimedMessages
|
||||
describe "chat item expiration" $ do
|
||||
it "set chat item TTL" testSetChatItemTTL
|
||||
describe "queue rotation" $ do
|
||||
@@ -247,13 +259,14 @@ testAddContact :: Spec
|
||||
testAddContact = versionTestMatrix2 runTestAddContact
|
||||
where
|
||||
runTestAddContact alice bob = do
|
||||
alice ##> "/c"
|
||||
alice ##> "/_connect 1"
|
||||
inv <- getInvitation alice
|
||||
bob ##> ("/c " <> inv)
|
||||
bob ##> ("/_connect 1 " <> inv)
|
||||
bob <## "confirmation sent!"
|
||||
concurrently_
|
||||
(bob <## "alice (Alice): contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
threadDelay 100000
|
||||
chatsEmpty alice bob
|
||||
alice #> "@bob hello there 🙂"
|
||||
bob <# "alice> hello there 🙂"
|
||||
@@ -330,7 +343,7 @@ testDeleteContactDeletesProfile =
|
||||
-- alice deletes contact, profile is deleted
|
||||
alice ##> "/d bob"
|
||||
alice <## "bob: contact is deleted"
|
||||
alice ##> "/contacts"
|
||||
alice ##> "/_contacts 1"
|
||||
(alice </)
|
||||
alice `hasContactProfiles` ["alice"]
|
||||
-- bob deletes contact, profile is deleted
|
||||
@@ -1859,7 +1872,7 @@ testUpdateProfileImage =
|
||||
alice <## "profile image updated"
|
||||
alice ##> "/profile_image"
|
||||
alice <## "profile image removed"
|
||||
alice ##> "/_profile {\"displayName\": \"alice2\", \"fullName\": \"\"}"
|
||||
alice ##> "/_profile 1 {\"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"
|
||||
@@ -2385,9 +2398,8 @@ testFilesFoldersImageSndDelete =
|
||||
checkActionDeletesFile "./tests/tmp/alice_app_files/test_1MB.pdf" $ do
|
||||
alice ##> "/d bob"
|
||||
alice <## "bob: contact is deleted"
|
||||
bob <## "alice cancelled sending file 1 (test_1MB.pdf)"
|
||||
bob ##> "/fs 1"
|
||||
bob <## "receiving file 1 (test_1MB.pdf) cancelled, received part path: test_1MB.pdf"
|
||||
bob <##. "receiving file 1 (test_1MB.pdf) progress"
|
||||
-- deleting contact should remove cancelled file
|
||||
checkActionDeletesFile "./tests/tmp/bob_app_files/test_1MB.pdf" $ do
|
||||
bob ##> "/d alice"
|
||||
@@ -2608,6 +2620,7 @@ testUserContactLink = versionTestMatrix3 $ \alice bob cath -> do
|
||||
concurrently_
|
||||
(bob <## "alice (Alice): contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
threadDelay 100000
|
||||
alice @@@ [("@bob", "Voice messages: enabled")]
|
||||
alice <##> bob
|
||||
|
||||
@@ -2619,6 +2632,7 @@ testUserContactLink = versionTestMatrix3 $ \alice bob cath -> do
|
||||
concurrently_
|
||||
(cath <## "alice (Alice): contact is connected")
|
||||
(alice <## "cath (Catherine): contact is connected")
|
||||
threadDelay 100000
|
||||
alice @@@ [("@cath", "Voice messages: enabled"), ("@bob", "hey")]
|
||||
alice <##> cath
|
||||
|
||||
@@ -2721,6 +2735,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $
|
||||
concurrently_
|
||||
(cath <## "alice (Alice): contact is connected")
|
||||
(alice <## "cath (Catherine): contact is connected")
|
||||
threadDelay 100000
|
||||
alice @@@ [("@cath", "Voice messages: enabled"), ("@bob", "hey")]
|
||||
alice <##> cath
|
||||
|
||||
@@ -2793,13 +2808,14 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile
|
||||
concurrently_
|
||||
(cath <## "alice (Alice): contact is connected")
|
||||
(alice <## "cath (Catherine): contact is connected")
|
||||
threadDelay 100000
|
||||
alice @@@ [("@cath", "Voice messages: enabled"), ("@robert", "hey")]
|
||||
alice <##> cath
|
||||
|
||||
testRejectContactAndDeleteUserContact :: IO ()
|
||||
testRejectContactAndDeleteUserContact = testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
alice ##> "/ad"
|
||||
alice ##> "/_address 1"
|
||||
cLink <- getContactLink alice True
|
||||
bob ##> ("/c " <> cLink)
|
||||
alice <#? bob
|
||||
@@ -2807,12 +2823,12 @@ testRejectContactAndDeleteUserContact = testChat3 aliceProfile bobProfile cathPr
|
||||
alice <## "bob: contact request rejected"
|
||||
(bob </)
|
||||
|
||||
alice ##> "/sa"
|
||||
alice ##> "/_show_address 1"
|
||||
cLink' <- getContactLink alice False
|
||||
alice <## "auto_accept off"
|
||||
cLink' `shouldBe` cLink
|
||||
|
||||
alice ##> "/da"
|
||||
alice ##> "/_delete_address 1"
|
||||
alice <## "Your chat address is deleted - accepted contacts will remain connected."
|
||||
alice <## "To create a new chat address use /ad"
|
||||
|
||||
@@ -2846,7 +2862,7 @@ testAutoReplyMessage = testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
alice ##> "/ad"
|
||||
cLink <- getContactLink alice True
|
||||
alice ##> "/auto_accept on incognito=off text hello!"
|
||||
alice ##> "/_auto_accept 1 on incognito=off text hello!"
|
||||
alice <## "auto_accept on"
|
||||
alice <## "auto reply:"
|
||||
alice <## "hello!"
|
||||
@@ -3281,7 +3297,7 @@ testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathPr
|
||||
cath <## "alice (Alice): contact is connected"
|
||||
]
|
||||
alice <## "cath (Catherine): contact is connected"
|
||||
alice ##> "/_profile {\"displayName\": \"alice\", \"fullName\": \"\", \"preferences\": {\"fullDelete\": {\"allow\": \"always\"}}}"
|
||||
alice ##> "/_profile 1 {\"displayName\": \"alice\", \"fullName\": \"\", \"preferences\": {\"fullDelete\": {\"allow\": \"always\"}}}"
|
||||
alice <## "user full name removed (your contacts are notified)"
|
||||
alice <## "updated preferences:"
|
||||
alice <## "Full deletion allowed: always"
|
||||
@@ -3439,6 +3455,7 @@ testSetConnectionAlias = testChat2 aliceProfile bobProfile $
|
||||
concurrently_
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
(bob <## "alice (Alice): contact is connected")
|
||||
threadDelay 100000
|
||||
alice @@@ [("@bob", "Voice messages: enabled")]
|
||||
alice ##> "/contacts"
|
||||
alice <## "bob (Bob) (alias: friend)"
|
||||
@@ -3452,7 +3469,7 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $
|
||||
createDirectoryIfMissing True "./tests/tmp/bob"
|
||||
copyFile "./tests/fixtures/test.txt" "./tests/tmp/alice/test.txt"
|
||||
copyFile "./tests/fixtures/test.txt" "./tests/tmp/bob/test.txt"
|
||||
bob ##> "/_profile {\"displayName\": \"bob\", \"fullName\": \"Bob\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}"
|
||||
bob ##> "/_profile 1 {\"displayName\": \"bob\", \"fullName\": \"Bob\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}"
|
||||
bob <## "profile image removed"
|
||||
bob <## "updated preferences:"
|
||||
bob <## "Voice messages allowed: no"
|
||||
@@ -3489,7 +3506,7 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $
|
||||
alice <## "started receiving file 1 (test.txt) from bob"
|
||||
alice <## "completed receiving file 1 (test.txt) from bob"
|
||||
(bob </)
|
||||
-- alice ##> "/_profile {\"displayName\": \"alice\", \"fullName\": \"Alice\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}"
|
||||
-- alice ##> "/_profile 1 {\"displayName\": \"alice\", \"fullName\": \"Alice\", \"preferences\": {\"voice\": {\"allow\": \"no\"}}}"
|
||||
alice ##> "/set voice no"
|
||||
alice <## "updated preferences:"
|
||||
alice <## "Voice messages allowed: no"
|
||||
@@ -3502,7 +3519,7 @@ testSetContactPrefs = testChat2 aliceProfile bobProfile $
|
||||
bob <## "Voice messages: off (you allow: default (no), contact allows: yes)"
|
||||
bob #$> ("/_get chat @2 count=100", chat, startFeatures <> [(0, "Voice messages: enabled for you"), (1, "voice message (00:10)"), (0, "Voice messages: off")])
|
||||
(bob </)
|
||||
bob ##> "/_profile {\"displayName\": \"bob\", \"fullName\": \"\", \"preferences\": {\"voice\": {\"allow\": \"yes\"}}}"
|
||||
bob ##> "/_profile 1 {\"displayName\": \"bob\", \"fullName\": \"\", \"preferences\": {\"voice\": {\"allow\": \"yes\"}}}"
|
||||
bob <## "user full name removed (your contacts are notified)"
|
||||
bob <## "updated preferences:"
|
||||
bob <## "Voice messages allowed: yes"
|
||||
@@ -3815,7 +3832,7 @@ testGetSetSMPServers :: IO ()
|
||||
testGetSetSMPServers =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice _ -> do
|
||||
alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001")
|
||||
alice #$> ("/_smp 1", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001")
|
||||
alice #$> ("/smp smp://1234-w==@smp1.example.im", id, "ok")
|
||||
alice #$> ("/smp", id, "smp://1234-w==@smp1.example.im")
|
||||
alice #$> ("/smp smp://1234-w==:password@smp1.example.im", id, "ok")
|
||||
@@ -3829,14 +3846,14 @@ testTestSMPServerConnection :: IO ()
|
||||
testTestSMPServerConnection =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice _ -> do
|
||||
alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001"
|
||||
alice ##> "/smp test 1 smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001"
|
||||
alice <## "SMP server test passed"
|
||||
-- to test with password:
|
||||
-- alice <## "SMP server test failed at CreateQueue, error: SMP AUTH"
|
||||
-- alice <## "Server requires authorization to create queues, check password"
|
||||
alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001"
|
||||
alice ##> "/smp test 1 smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001"
|
||||
alice <## "SMP server test passed"
|
||||
alice ##> "/smp test smp://LcJU@localhost:5001"
|
||||
alice ##> "/smp test 1 smp://LcJU@localhost:5001"
|
||||
alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:5001 NETWORK"
|
||||
alice <## "Possibly, certificate fingerprint in server address is incorrect"
|
||||
|
||||
@@ -3844,6 +3861,7 @@ testAsyncInitiatingOffline :: IO ()
|
||||
testAsyncInitiatingOffline = withTmpFiles $ do
|
||||
putStrLn "testAsyncInitiatingOffline"
|
||||
inv <- withNewTestChat "alice" aliceProfile $ \alice -> do
|
||||
threadDelay 250000
|
||||
putStrLn "1"
|
||||
alice ##> "/c"
|
||||
putStrLn "2"
|
||||
@@ -4410,6 +4428,660 @@ testMuteGroup =
|
||||
bob ##> "/gs"
|
||||
bob <## "#team"
|
||||
|
||||
testCreateSecondUser :: IO ()
|
||||
testCreateSecondUser =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
connectUsers alice bob
|
||||
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
-- connect using second user
|
||||
connectUsers alice bob
|
||||
alice #> "@bob hello"
|
||||
bob <# "alisa> hello"
|
||||
bob #> "@alisa hey"
|
||||
alice <# "bob> hey"
|
||||
|
||||
alice ##> "/user"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
alice ##> "/users"
|
||||
alice <## "alice (Alice)"
|
||||
alice <## "alisa (active)"
|
||||
|
||||
-- receive message to first user
|
||||
bob #> "@alice hey alice"
|
||||
(alice, "alice") $<# "bob> hey alice"
|
||||
|
||||
connectUsers alice cath
|
||||
|
||||
-- set active user to first user
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
|
||||
alice ##> "/user"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
|
||||
alice ##> "/users"
|
||||
alice <## "alice (Alice) (active)"
|
||||
alice <## "alisa"
|
||||
|
||||
alice <##> bob
|
||||
|
||||
cath #> "@alisa hey alisa"
|
||||
(alice, "alisa") $<# "cath> hey alisa"
|
||||
alice ##> "@cath hi cath"
|
||||
alice <## "no contact cath"
|
||||
|
||||
-- set active user to second user
|
||||
alice ##> "/_user 2"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
testUsersSubscribeAfterRestart :: IO ()
|
||||
testUsersSubscribeAfterRestart = withTmpFiles $ do
|
||||
withNewTestChat "bob" bobProfile $ \bob -> do
|
||||
withNewTestChat "alice" aliceProfile $ \alice -> do
|
||||
connectUsers alice bob
|
||||
alice <##> bob
|
||||
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
connectUsers alice bob
|
||||
alice <##> bob
|
||||
|
||||
withTestChat "alice" $ \alice -> do
|
||||
-- second user is active
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
|
||||
|
||||
-- second user receives message
|
||||
alice <##> bob
|
||||
|
||||
-- first user receives message
|
||||
bob #> "@alice hey alice"
|
||||
(alice, "alice") $<# "bob> hey alice"
|
||||
|
||||
testMultipleUserAddresses :: IO ()
|
||||
testMultipleUserAddresses =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
alice ##> "/ad"
|
||||
cLinkAlice <- getContactLink alice True
|
||||
bob ##> ("/c " <> cLinkAlice)
|
||||
alice <#? bob
|
||||
alice @@@ [("<@bob", "")]
|
||||
alice ##> "/ac bob"
|
||||
alice <## "bob (Bob): accepting contact request..."
|
||||
concurrently_
|
||||
(bob <## "alice (Alice): contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
threadDelay 100000
|
||||
alice @@@ [("@bob", "Voice messages: enabled")]
|
||||
alice <##> bob
|
||||
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
-- connect using second user address
|
||||
alice ##> "/ad"
|
||||
cLinkAlisa <- getContactLink alice True
|
||||
bob ##> ("/c " <> cLinkAlisa)
|
||||
alice <#? bob
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("<@bob", "")])
|
||||
alice ##> "/ac bob"
|
||||
alice <## "bob (Bob): accepting contact request..."
|
||||
concurrently_
|
||||
(bob <## "alisa: contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", "Voice messages: enabled")])
|
||||
alice <##> bob
|
||||
|
||||
bob #> "@alice hey alice"
|
||||
(alice, "alice") $<# "bob> hey alice"
|
||||
|
||||
-- delete first user address
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
|
||||
alice ##> "/da"
|
||||
alice <## "Your chat address is deleted - accepted contacts will remain connected."
|
||||
alice <## "To create a new chat address use /ad"
|
||||
|
||||
-- second user receives request when not active
|
||||
cath ##> ("/c " <> cLinkAlisa)
|
||||
cath <## "connection request sent!"
|
||||
alice <## "[user: alisa] cath (Catherine) wants to connect to you!"
|
||||
alice <## "to accept: /ac cath"
|
||||
alice <## "to reject: /rc cath (the sender will NOT be notified)"
|
||||
|
||||
-- accept request
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
alice ##> "/ac cath"
|
||||
alice <## "cath (Catherine): accepting contact request..."
|
||||
concurrently_
|
||||
(cath <## "alisa: contact is connected")
|
||||
(alice <## "cath (Catherine): contact is connected")
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", "Voice messages: enabled"), ("@bob", "hey")])
|
||||
alice <##> cath
|
||||
|
||||
-- first user doesn't have cath as contact
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice @@@ [("@bob", "hey alice")]
|
||||
|
||||
testCreateUserDefaultServers :: IO ()
|
||||
testCreateUserDefaultServers =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice _ -> do
|
||||
alice #$> ("/smp smp://2345-w==@smp2.example.im;smp://3456-w==@smp3.example.im:5224", id, "ok")
|
||||
alice #$> ("/smp", id, "smp://2345-w==@smp2.example.im, smp://3456-w==@smp3.example.im:5224")
|
||||
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001")
|
||||
|
||||
-- with same_smp=off
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/smp", id, "smp://2345-w==@smp2.example.im, smp://3456-w==@smp3.example.im:5224")
|
||||
|
||||
alice ##> "/create user same_smp=off alisa2"
|
||||
showActiveUser alice "alisa2"
|
||||
|
||||
alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001")
|
||||
|
||||
testCreateUserSameServers :: IO ()
|
||||
testCreateUserSameServers =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice _ -> do
|
||||
alice #$> ("/smp smp://2345-w==@smp2.example.im;smp://3456-w==@smp3.example.im:5224", id, "ok")
|
||||
alice #$> ("/smp", id, "smp://2345-w==@smp2.example.im, smp://3456-w==@smp3.example.im:5224")
|
||||
|
||||
alice ##> "/create user same_smp=on alisa"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
alice #$> ("/smp", id, "smp://2345-w==@smp2.example.im, smp://3456-w==@smp3.example.im:5224")
|
||||
|
||||
testDeleteUser :: IO ()
|
||||
testDeleteUser =
|
||||
testChat4 aliceProfile bobProfile cathProfile danProfile $
|
||||
\alice bob cath dan -> do
|
||||
connectUsers alice bob
|
||||
|
||||
-- cannot delete active user
|
||||
|
||||
alice ##> "/_delete user 1 del_smp=off"
|
||||
alice <## "cannot delete active user"
|
||||
|
||||
-- delete user without deleting SMP queues
|
||||
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
connectUsers alice cath
|
||||
alice <##> cath
|
||||
|
||||
alice ##> "/users"
|
||||
alice <## "alice (Alice)"
|
||||
alice <## "alisa (active)"
|
||||
|
||||
alice ##> "/_delete user 1 del_smp=off"
|
||||
alice <## "ok"
|
||||
|
||||
alice ##> "/users"
|
||||
alice <## "alisa (active)"
|
||||
|
||||
bob #> "@alice hey"
|
||||
-- no connection authorization error - connection wasn't deleted
|
||||
(alice </)
|
||||
|
||||
-- cannot delete new active user
|
||||
|
||||
alice ##> "/delete user alisa"
|
||||
alice <## "cannot delete active user"
|
||||
|
||||
alice ##> "/users"
|
||||
alice <## "alisa (active)"
|
||||
|
||||
alice <##> cath
|
||||
|
||||
-- delete user deleting SMP queues
|
||||
|
||||
alice ##> "/create user alisa2"
|
||||
showActiveUser alice "alisa2"
|
||||
|
||||
connectUsers alice dan
|
||||
alice <##> dan
|
||||
|
||||
alice ##> "/users"
|
||||
alice <## "alisa"
|
||||
alice <## "alisa2 (active)"
|
||||
|
||||
alice ##> "/delete user alisa"
|
||||
alice <### ["ok", "completed deleting user"]
|
||||
|
||||
alice ##> "/users"
|
||||
alice <## "alisa2 (active)"
|
||||
|
||||
cath #> "@alisa hey"
|
||||
cath <## "[alisa, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection"
|
||||
(alice </)
|
||||
|
||||
alice <##> dan
|
||||
|
||||
testUsersDifferentCIExpirationTTL :: IO ()
|
||||
testUsersDifferentCIExpirationTTL = withTmpFiles $ do
|
||||
withNewTestChat "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatCfg cfg "alice" aliceProfile $ \alice -> do
|
||||
-- first user messages
|
||||
connectUsers alice bob
|
||||
|
||||
alice #> "@bob alice 1"
|
||||
bob <# "alice> alice 1"
|
||||
bob #> "@alice alice 2"
|
||||
alice <# "bob> alice 2"
|
||||
|
||||
-- second user messages
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
connectUsers alice bob
|
||||
|
||||
alice #> "@bob alisa 1"
|
||||
bob <# "alisa> alisa 1"
|
||||
bob #> "@alisa alisa 2"
|
||||
alice <# "bob> alisa 2"
|
||||
|
||||
-- set ttl for first user
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/_ttl 1 1", id, "ok")
|
||||
|
||||
-- set ttl for second user
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_ttl 2 3", id, "ok")
|
||||
|
||||
-- first user messages
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/ttl", id, "old messages are set to be deleted after: 1 second(s)")
|
||||
|
||||
alice #> "@bob alice 3"
|
||||
bob <# "alice> alice 3"
|
||||
bob #> "@alice alice 4"
|
||||
alice <# "bob> alice 4"
|
||||
|
||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "alice 1"), (0, "alice 2"), (1, "alice 3"), (0, "alice 4")])
|
||||
|
||||
-- second user messages
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/ttl", id, "old messages are set to be deleted after: 3 second(s)")
|
||||
|
||||
alice #> "@bob alisa 3"
|
||||
bob <# "alisa> alisa 3"
|
||||
bob #> "@alisa alisa 4"
|
||||
alice <# "bob> alisa 4"
|
||||
|
||||
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
|
||||
|
||||
threadDelay 2000000
|
||||
|
||||
-- messages both before and after setting chat item ttl are deleted
|
||||
-- first user messages
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/_get chat @2 count=100", chat, [])
|
||||
|
||||
-- second user messages
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
|
||||
|
||||
threadDelay 2000000
|
||||
|
||||
alice #$> ("/_get chat @4 count=100", chat, [])
|
||||
where
|
||||
cfg = testCfg {ciExpirationInterval = 500000}
|
||||
|
||||
testUsersRestartCIExpiration :: IO ()
|
||||
testUsersRestartCIExpiration = withTmpFiles $ do
|
||||
withNewTestChat "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatCfg cfg "alice" aliceProfile $ \alice -> do
|
||||
-- set ttl for first user
|
||||
alice #$> ("/_ttl 1 1", id, "ok")
|
||||
connectUsers alice bob
|
||||
|
||||
-- create second user and set ttl
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_ttl 2 3", id, "ok")
|
||||
connectUsers alice bob
|
||||
|
||||
-- first user messages
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
|
||||
alice #> "@bob alice 1"
|
||||
bob <# "alice> alice 1"
|
||||
bob #> "@alice alice 2"
|
||||
alice <# "bob> alice 2"
|
||||
|
||||
-- second user messages
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
alice #> "@bob alisa 1"
|
||||
bob <# "alisa> alisa 1"
|
||||
bob #> "@alisa alisa 2"
|
||||
alice <# "bob> alisa 2"
|
||||
|
||||
-- first user will be active on restart
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
|
||||
withTestChatCfg cfg "alice" $ \alice -> do
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
alice <## "[user: alisa] 1 contacts connected (use /cs for the list)"
|
||||
|
||||
-- first user messages
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/ttl", id, "old messages are set to be deleted after: 1 second(s)")
|
||||
|
||||
alice #> "@bob alice 3"
|
||||
bob <# "alice> alice 3"
|
||||
bob #> "@alice alice 4"
|
||||
alice <# "bob> alice 4"
|
||||
|
||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "alice 1"), (0, "alice 2"), (1, "alice 3"), (0, "alice 4")])
|
||||
|
||||
-- second user messages
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/ttl", id, "old messages are set to be deleted after: 3 second(s)")
|
||||
|
||||
alice #> "@bob alisa 3"
|
||||
bob <# "alisa> alisa 3"
|
||||
bob #> "@alisa alisa 4"
|
||||
alice <# "bob> alisa 4"
|
||||
|
||||
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
|
||||
|
||||
threadDelay 2000000
|
||||
|
||||
-- messages both before and after restart are deleted
|
||||
-- first user messages
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/_get chat @2 count=100", chat, [])
|
||||
|
||||
-- second user messages
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
|
||||
|
||||
threadDelay 2000000
|
||||
|
||||
alice #$> ("/_get chat @4 count=100", chat, [])
|
||||
where
|
||||
cfg = testCfg {ciExpirationInterval = 500000}
|
||||
|
||||
testEnableCIExpirationOnlyForOneUser :: IO ()
|
||||
testEnableCIExpirationOnlyForOneUser = withTmpFiles $ do
|
||||
withNewTestChat "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatCfg cfg "alice" aliceProfile $ \alice -> do
|
||||
-- first user messages
|
||||
connectUsers alice bob
|
||||
|
||||
alice #> "@bob alice 1"
|
||||
bob <# "alice> alice 1"
|
||||
bob #> "@alice alice 2"
|
||||
alice <# "bob> alice 2"
|
||||
|
||||
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "alice 1"), (0, "alice 2")])
|
||||
|
||||
-- second user messages before first user sets ttl
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
connectUsers alice bob
|
||||
|
||||
alice #> "@bob alisa 1"
|
||||
bob <# "alisa> alisa 1"
|
||||
bob #> "@alisa alisa 2"
|
||||
alice <# "bob> alisa 2"
|
||||
|
||||
-- set ttl for first user
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/_ttl 1 1", id, "ok")
|
||||
|
||||
-- second user messages after first user sets ttl
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
alice #> "@bob alisa 3"
|
||||
bob <# "alisa> alisa 3"
|
||||
bob #> "@alisa alisa 4"
|
||||
alice <# "bob> alisa 4"
|
||||
|
||||
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
|
||||
|
||||
threadDelay 2000000
|
||||
|
||||
-- messages are deleted for first user
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/_get chat @2 count=100", chat, [])
|
||||
|
||||
-- messages are not deleted for second user
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
|
||||
|
||||
withTestChatCfg cfg "alice" $ \alice -> do
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
|
||||
|
||||
-- messages are not deleted for second user after restart
|
||||
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
|
||||
|
||||
alice #> "@bob alisa 5"
|
||||
bob <# "alisa> alisa 5"
|
||||
bob #> "@alisa alisa 6"
|
||||
alice <# "bob> alisa 6"
|
||||
|
||||
threadDelay 2000000
|
||||
|
||||
-- new messages are not deleted for second user
|
||||
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4"), (1, "alisa 5"), (0, "alisa 6")])
|
||||
where
|
||||
cfg = testCfg {ciExpirationInterval = 500000}
|
||||
|
||||
testDisableCIExpirationOnlyForOneUser :: IO ()
|
||||
testDisableCIExpirationOnlyForOneUser = withTmpFiles $ do
|
||||
withNewTestChat "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatCfg cfg "alice" aliceProfile $ \alice -> do
|
||||
-- set ttl for first user
|
||||
alice #$> ("/_ttl 1 1", id, "ok")
|
||||
connectUsers alice bob
|
||||
|
||||
-- create second user and set ttl
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_ttl 2 1", id, "ok")
|
||||
connectUsers alice bob
|
||||
|
||||
-- first user disables expiration
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/ttl none", id, "ok")
|
||||
alice #$> ("/ttl", id, "old messages are not being deleted")
|
||||
|
||||
-- second user still has ttl configured
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/ttl", id, "old messages are set to be deleted after: 1 second(s)")
|
||||
|
||||
alice #> "@bob alisa 1"
|
||||
bob <# "alisa> alisa 1"
|
||||
bob #> "@alisa alisa 2"
|
||||
alice <# "bob> alisa 2"
|
||||
|
||||
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2")])
|
||||
|
||||
threadDelay 2000000
|
||||
|
||||
-- second user messages are deleted
|
||||
alice #$> ("/_get chat @4 count=100", chat, [])
|
||||
|
||||
withTestChatCfg cfg "alice" $ \alice -> do
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
|
||||
|
||||
-- second user still has ttl configured after restart
|
||||
alice #$> ("/ttl", id, "old messages are set to be deleted after: 1 second(s)")
|
||||
|
||||
alice #> "@bob alisa 3"
|
||||
bob <# "alisa> alisa 3"
|
||||
bob #> "@alisa alisa 4"
|
||||
alice <# "bob> alisa 4"
|
||||
|
||||
alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")])
|
||||
|
||||
threadDelay 2000000
|
||||
|
||||
-- second user messages are deleted
|
||||
alice #$> ("/_get chat @4 count=100", chat, [])
|
||||
where
|
||||
cfg = testCfg {ciExpirationInterval = 500000}
|
||||
|
||||
testUsersTimedMessages :: IO ()
|
||||
testUsersTimedMessages = withTmpFiles $ do
|
||||
withNewTestChat "bob" bobProfile $ \bob -> do
|
||||
withNewTestChat "alice" aliceProfile $ \alice -> do
|
||||
connectUsers alice bob
|
||||
configureTimedMessages alice bob "2" "1"
|
||||
|
||||
-- create second user and configure timed messages for contact
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
connectUsers alice bob
|
||||
configureTimedMessages alice bob "4" "2"
|
||||
|
||||
-- first user messages
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
|
||||
alice #> "@bob alice 1"
|
||||
bob <# "alice> alice 1"
|
||||
bob #> "@alice alice 2"
|
||||
alice <# "bob> alice 2"
|
||||
|
||||
-- second user messages
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
alice #> "@bob alisa 1"
|
||||
bob <# "alisa> alisa 1"
|
||||
bob #> "@alisa alisa 2"
|
||||
alice <# "bob> alisa 2"
|
||||
|
||||
-- messages are deleted after ttl
|
||||
threadDelay 500000
|
||||
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/_get chat @2 count=100", chat, [(1, "alice 1"), (0, "alice 2")])
|
||||
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")])
|
||||
|
||||
threadDelay 1000000
|
||||
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/_get chat @2 count=100", chat, [])
|
||||
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")])
|
||||
|
||||
threadDelay 1000000
|
||||
|
||||
alice ##> "/user"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_get chat @4 count=100", chat, [])
|
||||
|
||||
-- first user messages
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
|
||||
alice #> "@bob alice 3"
|
||||
bob <# "alice> alice 3"
|
||||
bob #> "@alice alice 4"
|
||||
alice <# "bob> alice 4"
|
||||
|
||||
-- second user messages
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
|
||||
alice #> "@bob alisa 3"
|
||||
bob <# "alisa> alisa 3"
|
||||
bob #> "@alisa alisa 4"
|
||||
alice <# "bob> alisa 4"
|
||||
|
||||
withTestChat "alice" $ \alice -> do
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
|
||||
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/_get chat @2 count=100", chat, [(1, "alice 3"), (0, "alice 4")])
|
||||
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")])
|
||||
|
||||
-- messages are deleted after restart
|
||||
threadDelay 1500000
|
||||
|
||||
alice ##> "/user alice"
|
||||
showActiveUser alice "alice (Alice)"
|
||||
alice #$> ("/_get chat @2 count=100", chat, [])
|
||||
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")])
|
||||
|
||||
threadDelay 1000000
|
||||
|
||||
alice ##> "/user"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_get chat @4 count=100", chat, [])
|
||||
where
|
||||
configureTimedMessages alice bob bobId ttl = do
|
||||
aliceName <- userName alice
|
||||
alice ##> ("/_set prefs @" <> bobId <> " {\"timedMessages\": {\"allow\": \"yes\", \"ttl\": " <> ttl <> "}}")
|
||||
alice <## "you updated preferences for bob:"
|
||||
alice <## ("Disappearing messages: off (you allow: yes (" <> ttl <> " sec), contact allows: no)")
|
||||
bob <## (aliceName <> " updated preferences for you:")
|
||||
bob <## ("Disappearing messages: off (you allow: no, contact allows: yes (" <> ttl <> " sec))")
|
||||
bob ##> ("/set disappear @" <> aliceName <> " yes")
|
||||
bob <## ("you updated preferences for " <> aliceName <> ":")
|
||||
bob <## ("Disappearing messages: enabled (you allow: yes (" <> ttl <> " sec), contact allows: yes (" <> ttl <> " sec))")
|
||||
alice <## "bob updated preferences for you:"
|
||||
alice <## ("Disappearing messages: enabled (you allow: yes (" <> ttl <> " sec), contact allows: yes (" <> ttl <> " sec))")
|
||||
alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY") -- to remove feature items
|
||||
|
||||
testSetChatItemTTL :: IO ()
|
||||
testSetChatItemTTL =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
@@ -4435,10 +5107,10 @@ testSetChatItemTTL =
|
||||
alice <# "bob> 4"
|
||||
alice #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((1, "1"), Nothing), ((0, "2"), Nothing), ((1, ""), Just "test.jpg"), ((1, "3"), Nothing), ((0, "4"), Nothing)])
|
||||
checkActionDeletesFile "./tests/tmp/app_files/test.jpg" $
|
||||
alice #$> ("/_ttl 2", id, "ok")
|
||||
alice #$> ("/_ttl 1 2", id, "ok")
|
||||
alice #$> ("/_get chat @2 count=100", chat, [(1, "3"), (0, "4")]) -- when expiration is turned on, first cycle is synchronous
|
||||
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "1"), (1, "2"), (0, ""), (0, "3"), (1, "4")])
|
||||
alice #$> ("/ttl", id, "old messages are set to be deleted after: 2 second(s)")
|
||||
alice #$> ("/_ttl 1", id, "old messages are set to be deleted after: 2 second(s)")
|
||||
alice #$> ("/ttl week", id, "ok")
|
||||
alice #$> ("/ttl", id, "old messages are set to be deleted after: one week")
|
||||
alice #$> ("/ttl none", id, "ok")
|
||||
@@ -4476,6 +5148,7 @@ testGroupLink =
|
||||
bob <## "alice (Alice): contact is connected"
|
||||
bob <## "#team: you joined the group"
|
||||
]
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chat #1 count=100", chat, [(0, "invited via your group link"), (0, "connected")])
|
||||
-- contacts connected via group link are not in chat previews
|
||||
alice @@@ [("#team", "connected")]
|
||||
@@ -4622,6 +5295,7 @@ testGroupLinkContactUsed =
|
||||
bob <## "#team: you joined the group"
|
||||
]
|
||||
-- sending/receiving a message marks contact as used
|
||||
threadDelay 100000
|
||||
alice @@@ [("#team", "connected")]
|
||||
bob @@@ [("#team", "connected")]
|
||||
alice #> "@bob hello"
|
||||
@@ -5052,7 +5726,7 @@ connectUsers cc1 cc2 = do
|
||||
showName :: TestCC -> IO String
|
||||
showName (TestCC ChatController {currentUser} _ _ _ _) = do
|
||||
Just User {localDisplayName, profile = LocalProfile {fullName}} <- readTVarIO currentUser
|
||||
pure . T.unpack $ localDisplayName <> " (" <> fullName <> ")"
|
||||
pure . T.unpack $ localDisplayName <> optionalFullName localDisplayName fullName
|
||||
|
||||
createGroup2 :: String -> TestCC -> TestCC -> IO ()
|
||||
createGroup2 gName cc1 cc2 = do
|
||||
@@ -5162,14 +5836,20 @@ itemId :: Int -> String
|
||||
itemId i = show $ length chatFeatures + i
|
||||
|
||||
(@@@) :: TestCC -> [(String, String)] -> Expectation
|
||||
(@@@) = getChats . map $ \(ldn, msg, _) -> (ldn, msg)
|
||||
(@@@) = getChats mapChats
|
||||
|
||||
mapChats :: [(String, String, Maybe ConnStatus)] -> [(String, String)]
|
||||
mapChats = map $ \(ldn, msg, _) -> (ldn, msg)
|
||||
|
||||
chats :: String -> [(String, String)]
|
||||
chats = mapChats . read
|
||||
|
||||
(@@@!) :: TestCC -> [(String, String, Maybe ConnStatus)] -> Expectation
|
||||
(@@@!) = getChats id
|
||||
|
||||
getChats :: (Eq a, Show a) => ([(String, String, Maybe ConnStatus)] -> [a]) -> TestCC -> [a] -> Expectation
|
||||
getChats f cc res = do
|
||||
cc ##> "/_get chats pcc=on"
|
||||
cc ##> "/_get chats 1 pcc=on"
|
||||
line <- getTermLine cc
|
||||
f (read line) `shouldMatchList` res
|
||||
|
||||
@@ -5236,6 +5916,9 @@ cc <# line = (dropTime <$> getTermLine cc) `shouldReturn` line
|
||||
(?<#) :: TestCC -> String -> Expectation
|
||||
cc ?<# line = (dropTime <$> getTermLine cc) `shouldReturn` "i " <> line
|
||||
|
||||
($<#) :: (TestCC, String) -> String -> Expectation
|
||||
(cc, uName) $<# line = (dropTime . dropUser uName <$> getTermLine cc) `shouldReturn` line
|
||||
|
||||
(</) :: TestCC -> Expectation
|
||||
(</) = (<// 500000)
|
||||
|
||||
@@ -5248,6 +5931,18 @@ cc1 <#? cc2 = do
|
||||
cc1 <## ("to accept: /ac " <> name)
|
||||
cc1 <## ("to reject: /rc " <> name <> " (the sender will NOT be notified)")
|
||||
|
||||
dropUser :: String -> String -> String
|
||||
dropUser uName msg = fromMaybe err $ dropUser_ uName msg
|
||||
where
|
||||
err = error $ "invalid user: " <> msg
|
||||
|
||||
dropUser_ :: String -> String -> Maybe String
|
||||
dropUser_ uName msg = do
|
||||
let userPrefix = "[user: " <> uName <> "] "
|
||||
if userPrefix `isPrefixOf` msg
|
||||
then Just $ drop (length userPrefix) msg
|
||||
else Nothing
|
||||
|
||||
dropTime :: String -> String
|
||||
dropTime msg = fromMaybe err $ dropTime_ msg
|
||||
where
|
||||
@@ -5307,3 +6002,9 @@ lastItemId :: TestCC -> IO String
|
||||
lastItemId cc = do
|
||||
cc ##> "/last_item_id"
|
||||
getTermLine cc
|
||||
|
||||
showActiveUser :: TestCC -> String -> Expectation
|
||||
showActiveUser cc name = do
|
||||
cc <## ("user profile: " <> name)
|
||||
cc <## "use /p <display name> [<full name>] to change it"
|
||||
cc <## "(the updated profile will be sent to all your contacts)"
|
||||
|
||||
+19
-16
@@ -7,7 +7,7 @@ import ChatTests
|
||||
import Control.Monad.Except
|
||||
import Simplex.Chat.Mobile
|
||||
import Simplex.Chat.Store
|
||||
import Simplex.Chat.Types (Profile (..))
|
||||
import Simplex.Chat.Types (AgentUserId (..), Profile (..))
|
||||
import Test.Hspec
|
||||
|
||||
mobileTests :: Spec
|
||||
@@ -25,16 +25,16 @@ noActiveUser = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"e
|
||||
|
||||
activeUserExists :: String
|
||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||
activeUserExists = "{\"resp\":{\"chatCmdError\":{\"chatError\":{\"error\":{\"errorType\":{\"activeUserExists\":{}}}}}}}"
|
||||
activeUserExists = "{\"resp\":{\"chatCmdError\":{\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"no\"},\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":true},\"chatError\":{\"errorStore\":{\"storeError\":{\"duplicateName\":{}}}}}}}"
|
||||
#else
|
||||
activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"activeUserExists\"}}}}"
|
||||
activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"no\"},\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":true},\"chatError\":{\"type\":\"errorStore\",\"storeError\":{\"type\":\"duplicateName\"}}}}"
|
||||
#endif
|
||||
|
||||
activeUser :: String
|
||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||
activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"no\"},\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":true}}}}"
|
||||
activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"no\"},\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":true}}}}"
|
||||
#else
|
||||
activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"no\"},\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":true}}}"
|
||||
activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"no\"},\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":true}}}"
|
||||
#endif
|
||||
|
||||
chatStarted :: String
|
||||
@@ -46,32 +46,35 @@ chatStarted = "{\"resp\":{\"type\":\"chatStarted\"}}"
|
||||
|
||||
contactSubSummary :: String
|
||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||
contactSubSummary = "{\"resp\":{\"contactSubSummary\":{\"contactSubscriptions\":[]}}}"
|
||||
contactSubSummary = "{\"resp\":{\"contactSubSummary\":{" <> userJSON <> ",\"contactSubscriptions\":[]}}}"
|
||||
#else
|
||||
contactSubSummary = "{\"resp\":{\"type\":\"contactSubSummary\",\"contactSubscriptions\":[]}}"
|
||||
contactSubSummary = "{\"resp\":{\"type\":\"contactSubSummary\"," <> userJSON <> ",\"contactSubscriptions\":[]}}"
|
||||
#endif
|
||||
|
||||
memberSubSummary :: String
|
||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||
memberSubSummary = "{\"resp\":{\"memberSubSummary\":{\"memberSubscriptions\":[]}}}"
|
||||
memberSubSummary = "{\"resp\":{\"memberSubSummary\":{" <> userJSON <> ",\"memberSubscriptions\":[]}}}"
|
||||
#else
|
||||
memberSubSummary = "{\"resp\":{\"type\":\"memberSubSummary\",\"memberSubscriptions\":[]}}"
|
||||
memberSubSummary = "{\"resp\":{\"type\":\"memberSubSummary\"," <> userJSON <> ",\"memberSubscriptions\":[]}}"
|
||||
#endif
|
||||
|
||||
userContactSubSummary :: String
|
||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||
userContactSubSummary = "{\"resp\":{\"userContactSubSummary\":{\"userContactSubscriptions\":[]}}}"
|
||||
userContactSubSummary = "{\"resp\":{\"userContactSubSummary\":{" <> userJSON <> ",\"userContactSubscriptions\":[]}}}"
|
||||
#else
|
||||
userContactSubSummary = "{\"resp\":{\"type\":\"userContactSubSummary\",\"userContactSubscriptions\":[]}}"
|
||||
userContactSubSummary = "{\"resp\":{\"type\":\"userContactSubSummary\"," <> userJSON <> ",\"userContactSubscriptions\":[]}}"
|
||||
#endif
|
||||
|
||||
pendingSubSummary :: String
|
||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||
pendingSubSummary = "{\"resp\":{\"pendingSubSummary\":{\"pendingSubscriptions\":[]}}}"
|
||||
pendingSubSummary = "{\"resp\":{\"pendingSubSummary\":{" <> userJSON <> ",\"pendingSubscriptions\":[]}}}"
|
||||
#else
|
||||
pendingSubSummary = "{\"resp\":{\"type\":\"pendingSubSummary\",\"pendingSubscriptions\":[]}}"
|
||||
pendingSubSummary = "{\"resp\":{\"type\":\"pendingSubSummary\"," <> userJSON <> ",\"pendingSubscriptions\":[]}}"
|
||||
#endif
|
||||
|
||||
userJSON :: String
|
||||
userJSON = "\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"no\"},\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":false}"
|
||||
|
||||
parsedMarkdown :: String
|
||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||
parsedMarkdown = "{\"formattedText\":[{\"format\":{\"bold\":{}},\"text\":\"hello\"}]}"
|
||||
@@ -85,7 +88,7 @@ testChatApiNoUser = withTmpFiles $ do
|
||||
Left (DBMErrorNotADatabase _) <- chatMigrateInit testDBPrefix "myKey"
|
||||
chatSendCmd cc "/u" `shouldReturn` noActiveUser
|
||||
chatSendCmd cc "/_start" `shouldReturn` noActiveUser
|
||||
chatSendCmd cc "/u alice Alice" `shouldReturn` activeUser
|
||||
chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUser
|
||||
chatSendCmd cc "/_start" `shouldReturn` chatStarted
|
||||
|
||||
testChatApi :: IO ()
|
||||
@@ -93,12 +96,12 @@ testChatApi = withTmpFiles $ do
|
||||
let dbPrefix = testDBPrefix <> "1"
|
||||
f = chatStoreFile dbPrefix
|
||||
st <- createChatStore f "myKey" True
|
||||
Right _ <- withTransaction st $ \db -> runExceptT $ createUser db aliceProfile {preferences = Nothing} True
|
||||
Right _ <- withTransaction st $ \db -> runExceptT $ createUserRecord db (AgentUserId 1) aliceProfile {preferences = Nothing} True
|
||||
Right cc <- chatMigrateInit dbPrefix "myKey"
|
||||
Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix ""
|
||||
Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "anotherKey"
|
||||
chatSendCmd cc "/u" `shouldReturn` activeUser
|
||||
chatSendCmd cc "/u alice Alice" `shouldReturn` activeUserExists
|
||||
chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists
|
||||
chatSendCmd cc "/_start" `shouldReturn` chatStarted
|
||||
chatRecvMsg cc `shouldReturn` contactSubSummary
|
||||
chatRecvMsg cc `shouldReturn` userContactSubSummary
|
||||
|
||||
Reference in New Issue
Block a user