desktop, android: pass remote host to API from the loaded objects, to prevent race conditions (#3397)

* desktop, android: pass remote host explicitely to API calls

* use remote host ID in model updates

* add remote host to chat console

* add remote host to notifications functions
This commit is contained in:
Evgeny Poberezkin 2023-11-20 10:20:10 +00:00 committed by GitHub
parent 68cbc605be
commit 5b7de8f8c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 853 additions and 776 deletions

View File

@ -126,7 +126,7 @@ fun processIntent(intent: Intent?) {
when (intent?.action) {
"android.intent.action.VIEW" -> {
val uri = intent.data
if (uri != null) connectIfOpenedViaUri(uri.toURI(), ChatModel)
if (uri != null) connectIfOpenedViaUri(chatModel.remoteHostId, uri.toURI(), ChatModel)
}
}
}

View File

@ -57,7 +57,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
updatingChatsMutex.withLock {
kotlin.runCatching {
val currentUserId = chatModel.currentUser.value?.userId
val chats = ArrayList(chatController.apiGetChats())
val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId))
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */
if (chatModel.currentUser.value?.userId == currentUserId) {
val currentChatId = chatModel.chatId.value

View File

@ -115,22 +115,23 @@ actual fun ActiveCallView() {
val call = chatModel.activeCall.value
if (call != null) {
Log.d(TAG, "has active call $call")
val callRh = call.remoteHostId
when (val r = apiMsg.resp) {
is WCallResponse.Capabilities -> withBGApi {
val callType = CallType(call.localMedia, r.capabilities)
chatModel.controller.apiSendCallInvitation(call.contact, callType)
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
}
is WCallResponse.Offer -> withBGApi {
chatModel.controller.apiSendCallOffer(call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
}
is WCallResponse.Answer -> withBGApi {
chatModel.controller.apiSendCallAnswer(call.contact, r.answer, r.iceCandidates)
chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates)
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
}
is WCallResponse.Ice -> withBGApi {
chatModel.controller.apiSendCallExtraInfo(call.contact, r.iceCandidates)
chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates)
}
is WCallResponse.Connection ->
try {
@ -139,7 +140,7 @@ actual fun ActiveCallView() {
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectedAt = Clock.System.now())
setCallSound(call.soundSpeaker, audioViaBluetooth)
}
withBGApi { chatModel.controller.apiCallStatus(call.contact, callStatus) }
withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) }
} catch (e: Error) {
Log.d(TAG,"call status ${r.state.connectionState} not used")
}

View File

@ -12,7 +12,8 @@ import chat.simplex.common.model.ChatModel
import chat.simplex.res.MR
@Composable
actual fun ConnectViaLinkView(m: ChatModel, close: () -> Unit) {
actual fun ConnectViaLinkView(m: ChatModel, rhId: Long?, close: () -> Unit) {
// TODO this should close if remote host changes in model
val selection = remember {
mutableStateOf(
runCatching { ConnectViaLinkTab.valueOf(m.controller.appPrefs.connectViaLinkTab.get()!!) }.getOrDefault(ConnectViaLinkTab.SCAN)
@ -31,10 +32,10 @@ actual fun ConnectViaLinkView(m: ChatModel, close: () -> Unit) {
Column(Modifier.weight(1f)) {
when (selection.value) {
ConnectViaLinkTab.SCAN -> {
ScanToConnectView(m, close)
ScanToConnectView(m, rhId, close)
}
ConnectViaLinkTab.PASTE -> {
PasteToConnectView(m, close)
PasteToConnectView(m, rhId, close)
}
}
}

View File

@ -7,13 +7,14 @@ import chat.simplex.common.model.ChatModel
import com.google.accompanist.permissions.rememberPermissionState
@Composable
actual fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
actual fun ScanToConnectView(chatModel: ChatModel, rhId: Long?, close: () -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest()
}
ConnectContactLayout(
chatModel = chatModel,
rhId = rhId,
incognitoPref = chatModel.controller.appPrefs.incognito,
close = close
)

View File

@ -7,10 +7,10 @@ import chat.simplex.common.model.ServerCfg
import com.google.accompanist.permissions.rememberPermissionState
@Composable
actual fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) {
actual fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest()
}
ScanProtocolServerLayout(onNext)
ScanProtocolServerLayout(rhId, onNext)
}

View File

@ -138,7 +138,7 @@ fun MainScreen() {
}
onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {}
onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel)
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel)
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel, null)
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
}
if (appPlatform.isAndroid) {

View File

@ -111,6 +111,7 @@ object ChatModel {
// remote controller
val remoteHosts = mutableStateListOf<RemoteHostInfo>()
val currentRemoteHost = mutableStateOf<RemoteHostInfo?>(null)
val remoteHostId: Long? get() = currentRemoteHost?.value?.remoteHostId
val newRemoteHostPairing = mutableStateOf<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null)
val remoteCtrlSession = mutableStateOf<RemoteCtrlSession?>(null)
@ -141,16 +142,17 @@ object ChatModel {
}
// toList() here is to prevent ConcurrentModificationException that is rarely happens but happens
fun hasChat(id: String): Boolean = chats.toList().firstOrNull { it.id == id } != null
fun hasChat(rhId: Long?, id: String): Boolean = chats.toList().firstOrNull { it.id == id && it.remoteHostId == rhId } != null
// TODO pass rhId?
fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id }
fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId }
fun getGroupMember(groupMemberId: Long): GroupMember? = groupMembers.firstOrNull { it.groupMemberId == groupMemberId }
private fun getChatIndex(id: String): Int = chats.toList().indexOfFirst { it.id == id }
private fun getChatIndex(rhId: Long?, id: String): Int = chats.toList().indexOfFirst { it.id == id && it.remoteHostId == rhId }
fun addChat(chat: Chat) = chats.add(index = 0, chat)
fun updateChatInfo(cInfo: ChatInfo) {
val i = getChatIndex(cInfo.id)
fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) {
val i = getChatIndex(rhId, cInfo.id)
if (i >= 0) {
val currentCInfo = chats[i].chatInfo
var newCInfo = cInfo
@ -172,23 +174,23 @@ object ChatModel {
}
}
fun updateContactConnection(contactConnection: PendingContactConnection) = updateChat(ChatInfo.ContactConnection(contactConnection))
fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection))
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
fun updateContactConnectionStats(contact: Contact, connectionStats: ConnectionStats) {
fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) {
val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats)
val updatedContact = contact.copy(activeConn = updatedConn)
updateContact(updatedContact)
updateContact(rhId, updatedContact)
}
fun updateGroup(groupInfo: GroupInfo) = updateChat(ChatInfo.Group(groupInfo))
fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo))
private fun updateChat(cInfo: ChatInfo, addMissing: Boolean = true) {
if (hasChat(cInfo.id)) {
updateChatInfo(cInfo)
private fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) {
if (hasChat(rhId, cInfo.id)) {
updateChatInfo(rhId, cInfo)
} else if (addMissing) {
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf()))
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf()))
}
}
@ -203,8 +205,8 @@ object ChatModel {
}
}
fun replaceChat(id: String, chat: Chat) {
val i = getChatIndex(id)
fun replaceChat(rhId: Long?, id: String, chat: Chat) {
val i = getChatIndex(rhId, id)
if (i >= 0) {
chats[i] = chat
} else {
@ -213,9 +215,9 @@ object ChatModel {
}
}
suspend fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock {
suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock {
// update previews
val i = getChatIndex(cInfo.id)
val i = getChatIndex(rhId, cInfo.id)
val chat: Chat
if (i >= 0) {
chat = chats[i]
@ -224,7 +226,7 @@ object ChatModel {
chatStats =
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId
increaseUnreadCounter(currentUser.value!!)
increaseUnreadCounter(rhId, currentUser.value!!)
chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, minUnreadItemId = minUnreadId)
}
else
@ -234,7 +236,7 @@ object ChatModel {
popChat_(i)
}
} else {
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
}
Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
withContext(Dispatchers.Main) {
@ -254,9 +256,9 @@ object ChatModel {
}
}
suspend fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock {
suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock {
// update previews
val i = getChatIndex(cInfo.id)
val i = getChatIndex(rhId, cInfo.id)
val chat: Chat
val res: Boolean
if (i >= 0) {
@ -266,12 +268,12 @@ object ChatModel {
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
if (pItem.isRcvNew && !cItem.isRcvNew) {
// status changed from New to Read, update counter
decreaseCounterInChat(cInfo.id)
decreaseCounterInChat(rhId, cInfo.id)
}
}
res = false
} else {
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
res = true
}
Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
@ -313,12 +315,12 @@ object ChatModel {
}
}
fun removeChatItem(cInfo: ChatInfo, cItem: ChatItem) {
fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
if (cItem.isRcvNew) {
decreaseCounterInChat(cInfo.id)
decreaseCounterInChat(rhId, cInfo.id)
}
// update previews
val i = getChatIndex(cInfo.id)
val i = getChatIndex(rhId, cInfo.id)
val chat: Chat
if (i >= 0) {
chat = chats[i]
@ -337,11 +339,11 @@ object ChatModel {
}
}
fun clearChat(cInfo: ChatInfo) {
fun clearChat(rhId: Long?, cInfo: ChatInfo) {
// clear preview
val i = getChatIndex(cInfo.id)
val i = getChatIndex(rhId, cInfo.id)
if (i >= 0) {
decreaseUnreadCounter(currentUser.value!!, chats[i].chatStats.unreadCount)
decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount)
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
}
// clear current chat
@ -351,15 +353,15 @@ object ChatModel {
}
}
fun updateCurrentUser(newProfile: Profile, preferences: FullChatPreferences? = null) {
fun updateCurrentUser(rhId: Long?, 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)
val i = users.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId }
if (i != -1) {
users[i] = users[i].copy(user = updated)
}
currentUser.value = updated
}
@ -378,16 +380,17 @@ object ChatModel {
}
}
fun markChatItemsRead(cInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
val markedRead = markItemsReadInCurrentChat(cInfo, range)
fun markChatItemsRead(chat: Chat, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
val cInfo = chat.chatInfo
val markedRead = markItemsReadInCurrentChat(chat, range)
// update preview
val chatIdx = getChatIndex(cInfo.id)
val chatIdx = getChatIndex(chat.remoteHostId, cInfo.id)
if (chatIdx >= 0) {
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)
decreaseUnreadCounter(chat.remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
chats[chatIdx] = chat.copy(
chatStats = chat.chatStats.copy(
unreadCount = unreadCount,
@ -399,7 +402,8 @@ object ChatModel {
}
}
private fun markItemsReadInCurrentChat(cInfo: ChatInfo, range: CC.ItemRange? = null): Int {
private fun markItemsReadInCurrentChat(chat: Chat, range: CC.ItemRange? = null): Int {
val cInfo = chat.chatInfo
var markedRead = 0
if (chatId.value == cInfo.id) {
var i = 0
@ -423,13 +427,13 @@ object ChatModel {
return markedRead
}
private fun decreaseCounterInChat(chatId: ChatId) {
val chatIndex = getChatIndex(chatId)
private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) {
val chatIndex = getChatIndex(rhId, chatId)
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)
decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
chats[chatIndex] = chat.copy(
chatStats = chat.chatStats.copy(
unreadCount = unreadCount,
@ -437,18 +441,18 @@ object ChatModel {
)
}
fun increaseUnreadCounter(user: UserLike) {
changeUnreadCounter(user, 1)
fun increaseUnreadCounter(rhId: Long?, user: UserLike) {
changeUnreadCounter(rhId, user, 1)
}
fun decreaseUnreadCounter(user: UserLike, by: Int = 1) {
changeUnreadCounter(user, -by)
fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) {
changeUnreadCounter(rhId, user, -by)
}
private fun changeUnreadCounter(user: UserLike, by: Int) {
val i = users.indexOfFirst { it.user.userId == user.userId }
private fun changeUnreadCounter(rhId: Long?, user: UserLike, by: Int) {
val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId }
if (i != -1) {
users[i] = UserInfo(users[i].user, users[i].unreadCount + by)
users[i] = users[i].copy(unreadCount = users[i].unreadCount + by)
}
}
@ -544,14 +548,14 @@ object ChatModel {
}
}
fun removeChat(id: String) {
chats.removeAll { it.id == id }
fun removeChat(rhId: Long?, id: String) {
chats.removeAll { it.id == id && it.remoteHostId == rhId }
}
fun upsertGroupMember(groupInfo: GroupInfo, member: GroupMember): Boolean {
fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean {
// user member was updated
if (groupInfo.membership.groupMemberId == member.groupMemberId) {
updateGroup(groupInfo)
updateGroup(rhId, groupInfo)
return false
}
// update current chat
@ -569,12 +573,12 @@ object ChatModel {
}
}
fun updateGroupMemberConnectionStats(groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) {
fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) {
val memberConn = member.activeConn
if (memberConn != null) {
val updatedConn = memberConn.copy(connectionStats = connectionStats)
val updatedMember = member.copy(activeConn = updatedConn)
upsertGroupMember(groupInfo, updatedMember)
upsertGroupMember(rhId, groupInfo, updatedMember)
}
}
@ -612,6 +616,7 @@ enum class ChatType(val type: String) {
@Serializable
data class User(
val remoteHostId: Long? = null,
override val userId: Long,
val userContactId: Long,
val localDisplayName: String,
@ -711,9 +716,10 @@ interface SomeChat {
@Serializable @Stable
data class Chat (
val remoteHostId: Long? = null,
val chatInfo: ChatInfo,
val chatItems: List<ChatItem>,
val chatStats: ChatStats = ChatStats(),
val chatStats: ChatStats = ChatStats()
) {
val userCanSend: Boolean
get() = when (chatInfo) {

View File

@ -53,7 +53,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
} else if (startChat) {
// If we migrated successfully means previous re-encryption process on database level finished successfully too
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
val user = chatController.apiGetActiveUser()
val user = chatController.apiGetActiveUser(null)
if (user == null) {
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)

View File

@ -49,7 +49,8 @@ abstract class NtfManager {
null
}
val apiId = chatId.replace("<@", "").toLongOrNull() ?: return
acceptContactRequest(incognito, apiId, cInfo, isCurrentUser, ChatModel)
// TODO include remote host in notification
acceptContactRequest(null, incognito, apiId, cInfo, isCurrentUser, ChatModel)
cancelNotificationsForChat(chatId)
}
@ -57,11 +58,12 @@ abstract class NtfManager {
withBGApi {
awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
chatModel.controller.changeActiveUser(userId, null)
// TODO include remote host ID in desktop notifications?
chatModel.controller.changeActiveUser(null, userId, null)
}
val cInfo = chatModel.getChat(chatId)?.chatInfo
chatModel.clearOverlays.value = true
if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(cInfo, chatModel)
if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(null, cInfo, chatModel)
}
}
@ -69,7 +71,8 @@ abstract class NtfManager {
withBGApi {
awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
chatModel.controller.changeActiveUser(userId, null)
// TODO include remote host ID in desktop notifications?
chatModel.controller.changeActiveUser(null, userId, null)
}
chatModel.chatId.value = null
chatModel.clearOverlays.value = true

View File

@ -47,13 +47,14 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
val s = composeState.value.message
if (s.startsWith("/sql") && (!prefPerformLA || !developerTools)) {
val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.CommandError("Failed reading: empty")))
chatModel.addTerminalItem(TerminalItem.cmd(CC.Console(s)))
chatModel.addTerminalItem(TerminalItem.resp(resp))
chatModel.addTerminalItem(TerminalItem.cmd(null, CC.Console(s)))
chatModel.addTerminalItem(TerminalItem.resp(null, resp))
composeState.value = ComposeState(useLinkPreviews = false)
} else {
withApi {
// show "in progress"
chatModel.controller.sendCmd(CC.Console(s))
// TODO show active remote host in chat console?
chatModel.controller.sendCmd(chatModel.remoteHostId, CC.Console(s))
composeState.value = ComposeState(useLinkPreviews = false)
// hide "in progress"
}
@ -139,8 +140,10 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
val clipboard = LocalClipboardManager.current
LazyColumn(state = listState, reverseLayout = true) {
items(reversedTerminalItems) { item ->
val rhId = item.remoteHostId
val rhIdStr = if (rhId == null) "" else "$rhId "
Text(
"${item.date.toString().subSequence(11, 19)} ${item.label}",
"$rhIdStr${item.date.toString().subSequence(11, 19)} ${item.label}",
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 18.sp, color = MaterialTheme.colors.primary),
maxLines = 1,
overflow = TextOverflow.Ellipsis,

View File

@ -170,18 +170,19 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) {
withApi {
val rhId = chatModel.remoteHostId
val user = chatModel.controller.apiCreateActiveUser(
Profile(displayName.trim(), "", null)
rhId, Profile(displayName.trim(), "", null)
) ?: return@withApi
chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user)
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
} else {
val users = chatModel.controller.listUsers()
val users = chatModel.controller.listUsers(rhId)
chatModel.users.clear()
chatModel.users.addAll(users)
chatModel.controller.getUserChatData()
chatModel.controller.getUserChatData(rhId)
close()
}
}
@ -190,7 +191,7 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: ()
fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) {
withApi {
chatModel.controller.apiCreateActiveUser(
Profile(displayName.trim(), "", null)
null, Profile(displayName.trim(), "", null)
) ?: return@withApi
val onboardingStage = chatModel.controller.appPrefs.onboardingStage
if (chatModel.users.isEmpty()) {

View File

@ -43,6 +43,7 @@ class CallManager(val chatModel: ChatModel) {
private fun justAcceptIncomingCall(invitation: RcvCallInvitation) {
with (chatModel) {
activeCall.value = Call(
remoteHostId = invitation.remoteHostId,
contact = invitation.contact,
callState = CallState.InvitationAccepted,
localMedia = invitation.callType.media,
@ -76,7 +77,7 @@ class CallManager(val chatModel: ChatModel) {
Log.d(TAG, "CallManager.endCall: ending call...")
callCommand.add(WCallCommand.End)
showCallView.value = false
controller.apiEndCall(call.contact)
controller.apiEndCall(call.remoteHostId, call.contact)
activeCall.value = null
}
}
@ -90,7 +91,7 @@ class CallManager(val chatModel: ChatModel) {
ntfManager.cancelCallNotification()
}
withApi {
if (!controller.apiRejectCall(invitation.contact)) {
if (!controller.apiRejectCall(invitation.remoteHostId, invitation.contact)) {
Log.e(TAG, "apiRejectCall error")
}
}

View File

@ -11,6 +11,7 @@ import java.util.*
import kotlin.collections.ArrayList
data class Call(
val remoteHostId: Long? = null,
val contact: Contact,
val callState: CallState,
val localMedia: CallMediaType,
@ -95,7 +96,14 @@ sealed class WCallResponse {
@Serializable data class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
@Serializable data class WebRTCExtraInfo(val rtcIceCandidates: String)
@Serializable data class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
@Serializable data class RcvCallInvitation(val user: User, val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
@Serializable data class RcvCallInvitation(
val remoteHostId: Long? = null,
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) MR.strings.video_call_no_encryption else MR.strings.encrypted_video_call
CallMediaType.Audio -> if (sharedKey == null) MR.strings.audio_call_no_encryption else MR.strings.encrypted_audio_call

View File

@ -61,6 +61,7 @@ fun ChatInfoView(
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) {
mutableStateOf(chatModel.contactNetworkStatus(contact))
}
val chatRh = chat.remoteHostId
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
ChatInfoLayout(
chat,
@ -81,25 +82,25 @@ fun ChatInfoView(
connectionCode,
developerTools,
onLocalAliasChanged = {
setContactAlias(chat.chatInfo.apiId, it, chatModel)
setContactAlias(chat, it, chatModel)
},
openPreferences = {
ModalManager.end.showCustomModal { close ->
val user = chatModel.currentUser.value
if (user != null) {
ContactPreferencesView(chatModel, user, contact.contactId, close)
ContactPreferencesView(chatModel, user, chatRh, contact.contactId, close)
}
}
},
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
deleteContact = { deleteContactDialog(chat, chatModel, close) },
clearChat = { clearChatDialog(chat, chatModel, close) },
switchContactAddress = {
showSwitchAddressAlert(switchAddress = {
withApi {
val cStats = chatModel.controller.apiSwitchContact(contact.contactId)
val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId)
connStats.value = cStats
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
}
close.invoke()
}
@ -108,20 +109,20 @@ fun ChatInfoView(
abortSwitchContactAddress = {
showAbortSwitchAddressAlert(abortSwitchAddress = {
withApi {
val cStats = chatModel.controller.apiAbortSwitchContact(contact.contactId)
val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId)
connStats.value = cStats
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
}
}
})
},
syncContactConnection = {
withApi {
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = false)
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
connStats.value = cStats
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
}
close.invoke()
}
@ -129,10 +130,10 @@ fun ChatInfoView(
syncContactConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = {
withApi {
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = true)
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true)
connStats.value = cStats
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
}
close.invoke()
}
@ -146,9 +147,10 @@ fun ChatInfoView(
connectionCode,
ct.verified,
verify = { code ->
chatModel.controller.apiVerifyContact(ct.contactId, code)?.let { r ->
chatModel.controller.apiVerifyContact(chatRh, ct.contactId, code)?.let { r ->
val (verified, existingCode) = r
chatModel.updateContact(
chatRh,
ct.copy(
activeConn = ct.activeConn?.copy(
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
@ -195,7 +197,8 @@ sealed class SendReceipts {
}
}
fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
val chatInfo = chat.chatInfo
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.delete_contact_question),
text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)),
@ -206,7 +209,7 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
deleteContact(chatInfo, chatModel, close, notify = true)
deleteContact(chat, chatModel, close, notify = true)
}
}) {
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
@ -215,7 +218,7 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
deleteContact(chatInfo, chatModel, close, notify = false)
deleteContact(chat, chatModel, close, notify = false)
}
}) {
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
@ -225,7 +228,7 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
deleteContact(chatInfo, chatModel, close)
deleteContact(chat, chatModel, close)
}
}) {
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
@ -242,11 +245,13 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
)
}
fun deleteContact(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) {
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) {
val chatInfo = chat.chatInfo
withApi {
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId, notify)
val chatRh = chat.remoteHostId
val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify)
if (r) {
chatModel.removeChat(chatInfo.id)
chatModel.removeChat(chatRh, chatInfo.id)
if (chatModel.chatId.value == chatInfo.id) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
@ -257,16 +262,18 @@ fun deleteContact(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)?
}
}
fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
fun clearChatDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
val chatInfo = chat.chatInfo
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.clear_chat_question),
text = generalGetString(MR.strings.clear_chat_warning),
confirmText = generalGetString(MR.strings.clear_verb),
onConfirm = {
withApi {
val updatedChatInfo = chatModel.controller.apiClearChat(chatInfo.chatType, chatInfo.apiId)
val chatRh = chat.remoteHostId
val updatedChatInfo = chatModel.controller.apiClearChat(chatRh, chatInfo.chatType, chatInfo.apiId)
if (updatedChatInfo != null) {
chatModel.clearChat(updatedChatInfo)
chatModel.clearChat(chatRh, updatedChatInfo)
ntfManager.cancelNotificationsForChat(chatInfo.id)
close?.invoke()
}
@ -669,9 +676,10 @@ fun ShareAddressButton(onClick: () -> Unit) {
)
}
private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel) = withApi {
chatModel.controller.apiSetContactAlias(contactApiId, localAlias)?.let {
chatModel.updateContact(it)
private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withApi {
val chatRh = chat.remoteHostId
chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let {
chatModel.updateContact(chatRh, it)
}
}

View File

@ -46,7 +46,6 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
val activeChat = remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }) }
val searchText = rememberSaveable { mutableStateOf("") }
val user = chatModel.currentUser.value
val rhId = remember { chatModel.currentRemoteHost }.value?.remoteHostId
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val composeState = rememberSaveable(saver = ComposeState.saver()) {
mutableStateOf(
@ -101,11 +100,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}
}
val view = LocalMultiplatformView()
if (activeChat.value == null || user == null) {
val chat = activeChat.value
if (chat == null || user == null) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
} else {
val chat = activeChat.value!!
val chatRh = chat.remoteHostId
// We need to have real unreadCount value for displaying it inside top right button
// Having activeChat reloaded on every change in it is inefficient (UI lags)
val unreadCount = remember {
@ -167,11 +167,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
var preloadedCode: String? = null
var preloadedLink: Pair<String, GroupMemberRole>? = null
if (chat.chatInfo is ChatInfo.Direct) {
preloadedContactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
preloadedCode = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)?.second
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
} else if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
preloadedLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
}
ModalManager.end.showModalCloseable(true) { close ->
val chat = remember { activeChat }.value
@ -179,20 +179,20 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
var code: String? by remember { mutableStateOf(preloadedCode) }
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
preloadedContactInfo = contactInfo
code = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)?.second
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
preloadedCode = code
}
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
} else if (chat?.chatInfo is ChatInfo.Group) {
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
KeyChangeEffect(chat.id) {
setGroupMembers((chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
preloadedLink = link
}
GroupChatInfoView(chatModel, link?.first, link?.second, {
GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, {
link = it
preloadedLink = it
}, close)
@ -203,19 +203,19 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
hideKeyboard(view)
withApi {
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val (_, code) = if (member.memberActive) {
val memCode = chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId)
member to memCode?.second
} else {
member to null
}
setGroupMembers(groupInfo, chatModel)
setGroupMembers(chatRh, groupInfo, chatModel)
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { close ->
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, close, close)
GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close)
}
}
}
@ -226,7 +226,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
if (c != null && firstId != null) {
withApi {
Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}")
apiLoadPrevMessages(c.chatInfo, chatModel, firstId, searchText.value)
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}")
}
}
@ -242,6 +242,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
val toChatItem: ChatItem?
if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
val r = chatModel.controller.apiDeleteMemberChatItem(
chatRh,
groupId = groupInfo.groupId,
groupMemberId = groupMember.groupMemberId,
itemId = itemId
@ -250,6 +251,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
toChatItem = r?.second
} else {
val r = chatModel.controller.apiDeleteChatItem(
chatRh,
type = cInfo.chatType,
id = cInfo.apiId,
itemId = itemId,
@ -259,9 +261,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
toChatItem = r?.toChatItem?.chatItem
}
if (toChatItem == null && deletedChatItem != null) {
chatModel.removeChatItem(cInfo, deletedChatItem)
chatModel.removeChatItem(chatRh, cInfo, deletedChatItem)
} else if (toChatItem != null) {
chatModel.upsertChatItem(cInfo, toChatItem)
chatModel.upsertChatItem(chatRh, cInfo, toChatItem)
}
}
},
@ -272,27 +274,27 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
val deletedItems: ArrayList<ChatItem> = arrayListOf()
for (itemId in itemIds) {
val di = chatModel.controller.apiDeleteChatItem(
chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
)?.deletedChatItem?.chatItem
if (di != null) {
deletedItems.add(di)
}
}
for (di in deletedItems) {
chatModel.removeChatItem(chatInfo, di)
chatModel.removeChatItem(chatRh, chatInfo, di)
}
}
}
},
receiveFile = { fileId, encrypted ->
withApi { chatModel.controller.receiveFile(rhId, user, fileId, encrypted) }
withApi { chatModel.controller.receiveFile(chatRh, user, fileId, encrypted) }
},
cancelFile = { fileId ->
withApi { chatModel.controller.cancelFile(rhId, user, fileId) }
withApi { chatModel.controller.cancelFile(chatRh, user, fileId) }
},
joinGroup = { groupId, onComplete ->
withApi {
chatModel.controller.apiJoinGroup(groupId)
chatModel.controller.apiJoinGroup(chatRh, groupId)
onComplete.invoke()
}
},
@ -300,7 +302,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
withBGApi {
val cInfo = chat.chatInfo
if (cInfo is ChatInfo.Direct) {
chatModel.activeCall.value = Call(contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media)
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media)
chatModel.showCallView.value = true
chatModel.callCommand.add(WCallCommand.Capabilities(media))
}
@ -321,48 +323,48 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
},
acceptFeature = { contact, feature, param ->
withApi {
chatModel.controller.allowFeatureToContact(contact, feature, param)
chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param)
}
},
openDirectChat = { contactId ->
withApi {
openDirectChat(contactId, chatModel)
openDirectChat(chatRh, contactId, chatModel)
}
},
updateContactStats = { contact ->
withApi {
val r = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
if (r != null) {
val contactStats = r.first
if (contactStats != null)
chatModel.updateContactConnectionStats(contact, contactStats)
chatModel.updateContactConnectionStats(chatRh, contact, contactStats)
}
}
},
updateMemberStats = { groupInfo, member ->
withApi {
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
if (r != null) {
val memStats = r.second
if (memStats != null) {
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, memStats)
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
}
}
}
},
syncContactConnection = { contact ->
withApi {
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = false)
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
}
}
},
syncMemberConnection = { groupInfo, member ->
withApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = false)
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
}
}
},
@ -375,6 +377,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
setReaction = { cInfo, cItem, add, reaction ->
withApi {
val updatedCI = chatModel.controller.apiChatItemReaction(
rh = chatRh,
type = cInfo.chatType,
id = cInfo.apiId,
itemId = cItem.id,
@ -388,10 +391,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
},
showItemDetails = { cInfo, cItem ->
withApi {
val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id)
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
if (ciInfo != null) {
if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
}
ModalManager.end.closeModals()
ModalManager.end.showModal(endButtons = { ShareButton {
@ -405,28 +408,29 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
addMembers = { groupInfo ->
hideKeyboard(view)
withApi {
setGroupMembers(groupInfo, chatModel)
setGroupMembers(chatRh, groupInfo, chatModel)
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { close ->
AddGroupMembersView(groupInfo, false, chatModel, close)
AddGroupMembersView(chatRh, groupInfo, false, chatModel, close)
}
}
},
openGroupLink = { groupInfo ->
hideKeyboard(view)
withApi {
val link = chatModel.controller.apiGetGroupLink(groupInfo.groupId)
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) {
GroupLinkView(chatModel, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
}
}
},
markRead = { range, unreadCountAfter ->
chatModel.markChatItemsRead(chat.chatInfo, range, unreadCountAfter)
chatModel.markChatItemsRead(chat, range, unreadCountAfter)
ntfManager.cancelNotificationsForChat(chat.id)
withBGApi {
chatModel.controller.apiChatRead(
chatRh,
chat.chatInfo.chatType,
chat.chatInfo.apiId,
range
@ -438,7 +442,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
if (searchText.value == value) return@ChatLayout
val c = chatModel.getChat(chat.chatInfo.id) ?: return@ChatLayout
withApi {
apiFindMessages(c.chatInfo, chatModel, value)
apiFindMessages(c, chatModel, value)
searchText.value = value
}
},
@ -1254,14 +1258,16 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha
val chat = activeChat.value
if (chat?.chatStats?.unreadChat != true) return
withApi {
val chatRh = chat.remoteHostId
val success = chatModel.controller.apiChatUnread(
chatRh,
chat.chatInfo.chatType,
chat.chatInfo.apiId,
false
)
if (success && chat.id == activeChat.value?.id) {
activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))
chatModel.replaceChat(chat.id, activeChat.value!!)
chatModel.replaceChat(chatRh, chat.id, activeChat.value!!)
}
}
}

View File

@ -351,9 +351,10 @@ fun ComposeView(
}
}
suspend fun send(rhId: Long?, cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {
suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {
val cInfo = chat.chatInfo
val aChatItem = chatModel.controller.apiSendMessage(
rhId = rhId,
rh = chat.remoteHostId,
type = cInfo.chatType,
id = cInfo.apiId,
file = file,
@ -363,7 +364,7 @@ fun ComposeView(
ttl = ttl
)
if (aChatItem != null) {
chatModel.addChatItem(cInfo, aChatItem.chatItem)
chatModel.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
return aChatItem.chatItem
}
if (file != null) removeFile(file.filePath)
@ -410,23 +411,25 @@ fun ComposeView(
suspend fun sendMemberContactInvitation() {
val mc = checkLinkPreview()
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.chatInfo.apiId, mc)
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc)
if (contact != null) {
chatModel.updateContact(contact)
chatModel.updateContact(chat.remoteHostId, contact)
}
}
suspend fun updateMessage(ei: ChatItem, cInfo: ChatInfo, live: Boolean): ChatItem? {
suspend fun updateMessage(ei: ChatItem, chat: Chat, live: Boolean): ChatItem? {
val cInfo = chat.chatInfo
val oldMsgContent = ei.content.msgContent
if (oldMsgContent != null) {
val updatedItem = chatModel.controller.apiUpdateChatItem(
rh = chat.remoteHostId,
type = cInfo.chatType,
id = cInfo.apiId,
itemId = ei.meta.itemId,
mc = updateMsgContent(oldMsgContent),
live = live
)
if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem)
if (updatedItem != null) chatModel.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
return updatedItem?.chatItem
}
return null
@ -444,9 +447,9 @@ fun ComposeView(
sent = null
} else if (cs.contextItem is ComposeContextItem.EditingItem) {
val ei = cs.contextItem.chatItem
sent = updateMessage(ei, cInfo, live)
sent = updateMessage(ei, chat, live)
} else if (liveMessage != null && liveMessage.sent) {
sent = updateMessage(liveMessage.chatItem, cInfo, live)
sent = updateMessage(liveMessage.chatItem, chat, live)
} else {
val msgs: ArrayList<MsgContent> = ArrayList()
val files: ArrayList<CryptoFile> = ArrayList()
@ -528,7 +531,7 @@ fun ComposeView(
localPath = file.filePath
)
}
sent = send(remoteHost?.remoteHostId, cInfo, content, if (index == 0) quotedItemId else null, file,
sent = send(chat, content, if (index == 0) quotedItemId else null, file,
live = if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false,
ttl = ttl
)
@ -538,7 +541,7 @@ fun ComposeView(
cs.preview is ComposePreview.FilePreview ||
cs.preview is ComposePreview.VoicePreview)
) {
sent = send(remoteHost?.remoteHostId, cInfo, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
sent = send(chat, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
}
}
clearState(live)
@ -573,7 +576,7 @@ fun ComposeView(
fun allowVoiceToContact() {
val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return
withApi {
chatModel.controller.allowFeatureToContact(contact, ChatFeature.Voice)
chatModel.controller.allowFeatureToContact(chat.remoteHostId, contact, ChatFeature.Voice)
}
}

View File

@ -25,6 +25,7 @@ import chat.simplex.res.MR
fun ContactPreferencesView(
m: ChatModel,
user: User,
rhId: Long?,
contactId: Long,
close: () -> Unit,
) {
@ -36,9 +37,9 @@ fun ContactPreferencesView(
fun savePrefs(afterSave: () -> Unit = {}) {
withApi {
val prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
val toContact = m.controller.apiSetContactPrefs(ct.contactId, prefs)
val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs)
if (toContact != null) {
m.updateContact(toContact)
m.updateContact(rhId, toContact)
currentFeaturesAllowed = featuresAllowed
}
afterSave()

View File

@ -33,7 +33,7 @@ import chat.simplex.common.platform.*
import chat.simplex.res.MR
@Composable
fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) {
fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) {
val selectedContacts = remember { mutableStateListOf<Long>() }
val selectedRole = remember { mutableStateOf(GroupMemberRole.Member) }
var allowModifyMembers by remember { mutableStateOf(true) }
@ -49,16 +49,16 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
searchText,
openPreferences = {
ModalManager.end.showCustomModal { close ->
GroupPreferencesView(chatModel, groupInfo.id, close)
GroupPreferencesView(chatModel, rhId, groupInfo.id, close)
}
},
inviteMembers = {
allowModifyMembers = false
withApi {
for (contactId in selectedContacts) {
val member = chatModel.controller.apiAddMember(groupInfo.groupId, contactId, selectedRole.value)
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
if (member != null) {
chatModel.upsertGroupMember(groupInfo, member)
chatModel.upsertGroupMember(rhId, groupInfo, member)
} else {
break
}

View File

@ -41,9 +41,10 @@ import kotlinx.coroutines.launch
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
@Composable
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit) {
fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
// TODO derivedStateOf?
val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
val currentUser = chatModel.currentUser.value
val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) {
@ -68,25 +69,25 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
groupLink,
addMembers = {
withApi {
setGroupMembers(groupInfo, chatModel)
setGroupMembers(rhId, groupInfo, chatModel)
ModalManager.end.showModalCloseable(true) { close ->
AddGroupMembersView(groupInfo, false, chatModel, close)
AddGroupMembersView(rhId, groupInfo, false, chatModel, close)
}
}
},
showMemberInfo = { member ->
withApi {
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val (_, code) = if (member.memberActive) {
val memCode = chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
val memCode = chatModel.controller.apiGetGroupMemberCode(rhId, groupInfo.apiId, member.groupMemberId)
member to memCode?.second
} else {
member to null
}
ModalManager.end.showModalCloseable(true) { closeCurrent ->
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, closeCurrent) {
GroupMemberInfoView(rhId, groupInfo, mem, stats, code, chatModel, closeCurrent) {
closeCurrent()
close()
}
@ -95,31 +96,33 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
}
},
editGroupProfile = {
ModalManager.end.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) }
ModalManager.end.showCustomModal { close -> GroupProfileView(rhId, groupInfo, chatModel, close) }
},
addOrEditWelcomeMessage = {
ModalManager.end.showCustomModal { close -> GroupWelcomeView(chatModel, groupInfo, close) }
ModalManager.end.showCustomModal { close -> GroupWelcomeView(chatModel, rhId, groupInfo, close) }
},
openPreferences = {
ModalManager.end.showCustomModal { close ->
GroupPreferencesView(
chatModel,
rhId,
chat.id,
close
)
}
},
deleteGroup = { deleteGroupDialog(chat.chatInfo, groupInfo, chatModel, close) },
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
deleteGroup = { deleteGroupDialog(chat, groupInfo, chatModel, close) },
clearChat = { clearChatDialog(chat, chatModel, close) },
leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) },
manageGroupLink = {
ModalManager.end.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
}
)
}
}
fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
val chatInfo = chat.chatInfo
val alertTextKey =
if (groupInfo.membership.memberCurrent) MR.strings.delete_group_for_all_members_cannot_undo_warning
else MR.strings.delete_group_for_self_cannot_undo_warning
@ -129,9 +132,9 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM
confirmText = generalGetString(MR.strings.delete_verb),
onConfirm = {
withApi {
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId)
if (r) {
chatModel.removeChat(chatInfo.id)
chatModel.removeChat(chat.remoteHostId, chatInfo.id)
if (chatModel.chatId.value == chatInfo.id) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
@ -145,14 +148,14 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM
)
}
fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.leave_group_question),
text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
confirmText = generalGetString(MR.strings.leave_group_button),
onConfirm = {
withApi {
chatModel.controller.leaveGroup(groupInfo.groupId)
chatModel.controller.leaveGroup(rhId, groupInfo.groupId)
close?.invoke()
}
},
@ -160,16 +163,16 @@ fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> U
)
}
private fun removeMemberAlert(groupInfo: GroupInfo, mem: GroupMember) {
private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMember) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.button_remove_member_question),
text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone),
confirmText = generalGetString(MR.strings.remove_member_confirmation),
onConfirm = {
withApi {
val updatedMember = chatModel.controller.apiRemoveMember(groupInfo.groupId, mem.groupMemberId)
val updatedMember = chatModel.controller.apiRemoveMember(rhId, groupInfo.groupId, mem.groupMemberId)
if (updatedMember != null) {
chatModel.upsertGroupMember(groupInfo, updatedMember)
chatModel.upsertGroupMember(rhId, groupInfo, updatedMember)
}
}
},
@ -260,7 +263,7 @@ fun GroupChatInfoLayout(
Divider()
val showMenu = remember { mutableStateOf(false) }
SectionItemViewLongClickable({ showMemberInfo(member) }, { showMenu.value = true }, minHeight = 54.dp) {
DropDownMenuForMember(member, groupInfo, showMenu)
DropDownMenuForMember(chat.remoteHostId, member, groupInfo, showMenu)
MemberRow(member, onClick = { showMemberInfo(member) })
}
}
@ -413,22 +416,22 @@ private fun MemberVerifiedShield() {
}
@Composable
private fun DropDownMenuForMember(member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
DefaultDropdownMenu(showMenu) {
if (member.canBeRemoved(groupInfo)) {
ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
removeMemberAlert(groupInfo, member)
removeMemberAlert(rhId, groupInfo, member)
showMenu.value = false
})
}
if (member.memberSettings.showMessages) {
ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
blockMemberAlert(groupInfo, member)
blockMemberAlert(rhId, groupInfo, member)
showMenu.value = false
})
} else {
ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = {
unblockMemberAlert(groupInfo, member)
unblockMemberAlert(rhId, groupInfo, member)
showMenu.value = false
})
}

View File

@ -25,6 +25,7 @@ import chat.simplex.res.MR
@Composable
fun GroupLinkView(
chatModel: ChatModel,
rhId: Long?,
groupInfo: GroupInfo,
connReqContact: String?,
memberRole: GroupMemberRole?,
@ -38,7 +39,7 @@ fun GroupLinkView(
fun createLink() {
creatingLink = true
withApi {
val link = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
val link = chatModel.controller.apiCreateGroupLink(rhId, groupInfo.groupId)
if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
@ -62,7 +63,7 @@ fun GroupLinkView(
val role = groupLinkMemberRole.value
if (role != null) {
withBGApi {
val link = chatModel.controller.apiGroupLinkMemberRole(groupInfo.groupId, role)
val link = chatModel.controller.apiGroupLinkMemberRole(rhId, groupInfo.groupId, role)
if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
@ -78,7 +79,7 @@ fun GroupLinkView(
confirmText = generalGetString(MR.strings.delete_verb),
onConfirm = {
withApi {
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
val r = chatModel.controller.apiDeleteGroupLink(rhId, groupInfo.groupId)
if (r) {
groupLink = null
onGroupLinkUpdated?.invoke(null)

View File

@ -40,6 +40,7 @@ import kotlinx.datetime.Clock
@Composable
fun GroupMemberInfoView(
rhId: Long?,
groupInfo: GroupInfo,
member: GroupMember,
connectionStats: ConnectionStats?,
@ -49,7 +50,7 @@ fun GroupMemberInfoView(
closeAll: () -> Unit, // Close all open windows up to ChatView
) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId }
val connStats = remember { mutableStateOf(connectionStats) }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
var progressIndicator by remember { mutableStateOf(false) }
@ -66,7 +67,7 @@ fun GroupMemberInfoView(
getContactChat = { chatModel.getContactChat(it) },
openDirectChat = {
withApi {
val c = chatModel.controller.apiGetChat(ChatType.Direct, it)
val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it)
if (c != null) {
if (chatModel.getContactChat(it) == null) {
chatModel.addChat(c)
@ -82,9 +83,9 @@ fun GroupMemberInfoView(
createMemberContact = {
withApi {
progressIndicator = true
val memberContact = chatModel.controller.apiCreateMemberContact(groupInfo.apiId, member.groupMemberId)
val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId)
if (memberContact != null) {
val memberChat = Chat(ChatInfo.Direct(memberContact), chatItems = arrayListOf())
val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf())
chatModel.addChat(memberChat)
openLoadedChat(memberChat, chatModel)
closeAll()
@ -94,11 +95,11 @@ fun GroupMemberInfoView(
}
},
connectViaAddress = { connReqUri ->
connectViaMemberAddressAlert(connReqUri)
connectViaMemberAddressAlert(rhId, connReqUri)
},
blockMember = { blockMemberAlert(groupInfo, member) },
unblockMember = { unblockMemberAlert(groupInfo, member) },
removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) },
blockMember = { blockMemberAlert(rhId, groupInfo, member) },
unblockMember = { unblockMemberAlert(rhId, groupInfo, member) },
removeMember = { removeMemberDialog(rhId, groupInfo, member, chatModel, close) },
onRoleSelected = {
if (it == newRole.value) return@GroupMemberInfoLayout
val prevValue = newRole.value
@ -108,8 +109,8 @@ fun GroupMemberInfoView(
}) {
withApi {
kotlin.runCatching {
val mem = chatModel.controller.apiMemberRole(groupInfo.groupId, member.groupMemberId, it)
chatModel.upsertGroupMember(groupInfo, mem)
val mem = chatModel.controller.apiMemberRole(rhId, groupInfo.groupId, member.groupMemberId, it)
chatModel.upsertGroupMember(rhId, groupInfo, mem)
}.onFailure {
newRole.value = prevValue
}
@ -119,10 +120,10 @@ fun GroupMemberInfoView(
switchMemberAddress = {
showSwitchAddressAlert(switchAddress = {
withApi {
val r = chatModel.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
if (r != null) {
connStats.value = r.second
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
close.invoke()
}
}
@ -131,10 +132,10 @@ fun GroupMemberInfoView(
abortSwitchMemberAddress = {
showAbortSwitchAddressAlert(abortSwitchAddress = {
withApi {
val r = chatModel.controller.apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
if (r != null) {
connStats.value = r.second
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
close.invoke()
}
}
@ -142,10 +143,10 @@ fun GroupMemberInfoView(
},
syncMemberConnection = {
withApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = false)
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
connStats.value = r.second
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
close.invoke()
}
}
@ -153,10 +154,10 @@ fun GroupMemberInfoView(
syncMemberConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = {
withApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = true)
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true)
if (r != null) {
connStats.value = r.second
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
close.invoke()
}
}
@ -170,9 +171,10 @@ fun GroupMemberInfoView(
connectionCode,
mem.verified,
verify = { code ->
chatModel.controller.apiVerifyGroupMember(mem.groupId, mem.groupMemberId, code)?.let { r ->
chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r ->
val (verified, existingCode) = r
chatModel.upsertGroupMember(
rhId,
groupInfo,
mem.copy(
activeConn = mem.activeConn?.copy(
@ -196,16 +198,16 @@ fun GroupMemberInfoView(
}
}
fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.button_remove_member),
text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone),
confirmText = generalGetString(MR.strings.remove_member_confirmation),
onConfirm = {
withApi {
val removedMember = chatModel.controller.apiRemoveMember(member.groupId, member.groupMemberId)
val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId)
if (removedMember != null) {
chatModel.upsertGroupMember(groupInfo, removedMember)
chatModel.upsertGroupMember(rhId, groupInfo, removedMember)
}
close?.invoke()
}
@ -500,11 +502,11 @@ private fun updateMemberRoleDialog(
)
}
fun connectViaMemberAddressAlert(connReqUri: String) {
fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) {
try {
val uri = URI(connReqUri)
withApi {
planAndConnect(chatModel, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
planAndConnect(chatModel, rhId, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
}
} catch (e: RuntimeException) {
AlertManager.shared.showAlertMsg(
@ -514,39 +516,39 @@ fun connectViaMemberAddressAlert(connReqUri: String) {
}
}
fun blockMemberAlert(gInfo: GroupInfo, mem: GroupMember) {
fun blockMemberAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.block_member_question),
text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName),
confirmText = generalGetString(MR.strings.block_member_confirmation),
onConfirm = {
toggleShowMemberMessages(gInfo, mem, false)
toggleShowMemberMessages(rhId, gInfo, mem, false)
},
destructive = true,
)
}
fun unblockMemberAlert(gInfo: GroupInfo, mem: GroupMember) {
fun unblockMemberAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.unblock_member_question),
text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName),
confirmText = generalGetString(MR.strings.unblock_member_confirmation),
onConfirm = {
toggleShowMemberMessages(gInfo, mem, true)
toggleShowMemberMessages(rhId, gInfo, mem, true)
},
)
}
fun toggleShowMemberMessages(gInfo: GroupInfo, member: GroupMember, showMessages: Boolean) {
fun toggleShowMemberMessages(rhId: Long?, gInfo: GroupInfo, member: GroupMember, showMessages: Boolean) {
val updatedMemberSettings = member.memberSettings.copy(showMessages = showMessages)
updateMemberSettings(gInfo, member, updatedMemberSettings)
updateMemberSettings(rhId, gInfo, member, updatedMemberSettings)
}
fun updateMemberSettings(gInfo: GroupInfo, member: GroupMember, memberSettings: GroupMemberSettings) {
fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, memberSettings: GroupMemberSettings) {
withBGApi {
val success = ChatController.apiSetMemberSettings(gInfo.groupId, member.groupMemberId, memberSettings)
val success = ChatController.apiSetMemberSettings(rhId, gInfo.groupId, member.groupMemberId, memberSettings)
if (success) {
ChatModel.upsertGroupMember(gInfo, member.copy(memberSettings = memberSettings))
ChatModel.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
}
}
}

View File

@ -21,8 +21,12 @@ import chat.simplex.common.model.*
import chat.simplex.res.MR
@Composable
fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
val groupInfo = remember { derivedStateOf { (m.getChat(chatId)?.chatInfo as? ChatInfo.Group)?.groupInfo } }
fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> Unit,) {
val groupInfo = remember { derivedStateOf {
val ch = m.getChat(chatId)
val g = (ch?.chatInfo as? ChatInfo.Group)?.groupInfo
if (g == null || ch?.remoteHostId != rhId) null else g
}}
val gInfo = groupInfo.value ?: return
var preferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(gInfo.fullGroupPreferences) }
var currentPreferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(preferences) }
@ -30,9 +34,9 @@ fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
fun savePrefs(afterSave: () -> Unit = {}) {
withApi {
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
val g = m.controller.apiUpdateGroup(gInfo.groupId, gp)
val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp)
if (g != null) {
m.updateGroup(g)
m.updateGroup(rhId, g)
currentPreferences = preferences
}
afterSave()

View File

@ -30,15 +30,15 @@ import kotlinx.coroutines.launch
import java.net.URI
@Composable
fun GroupProfileView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) {
fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) {
GroupProfileLayout(
close = close,
groupProfile = groupInfo.groupProfile,
saveProfile = { p ->
withApi {
val gInfo = chatModel.controller.apiUpdateGroup(groupInfo.groupId, p)
val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p)
if (gInfo != null) {
chatModel.updateGroup(gInfo)
chatModel.updateGroup(rhId, gInfo)
close.invoke()
}
}

View File

@ -30,7 +30,7 @@ import chat.simplex.res.MR
import kotlinx.coroutines.delay
@Composable
fun GroupWelcomeView(m: ChatModel, groupInfo: GroupInfo, close: () -> Unit) {
fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) {
var gInfo by remember { mutableStateOf(groupInfo) }
val welcomeText = remember { mutableStateOf(gInfo.groupProfile.description ?: "") }
@ -41,10 +41,10 @@ fun GroupWelcomeView(m: ChatModel, groupInfo: GroupInfo, close: () -> Unit) {
welcome = null
}
val groupProfileUpdated = gInfo.groupProfile.copy(description = welcome)
val res = m.controller.apiUpdateGroup(gInfo.groupId, groupProfileUpdated)
val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated)
if (res != null) {
gInfo = res
m.updateGroup(res)
m.updateGroup(rhId, res)
welcomeText.value = welcome ?: ""
}
afterSave()

View File

@ -62,7 +62,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
ChatListNavLinkLayout(
chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode, inProgress = false, progressByTimeout = false) },
click = { directChatAction(chat.chatInfo.contact, chatModel) },
click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) },
dropdownMenuItems = { ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead) },
showMenu,
stopped,
@ -72,7 +72,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
is ChatInfo.Group ->
ChatListNavLinkLayout(
chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode, inProgress.value, progressByTimeout) },
click = { if (!inProgress.value) groupChatAction(chat.chatInfo.groupInfo, chatModel, inProgress) },
click = { if (!inProgress.value) groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) },
dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) },
showMenu,
stopped,
@ -81,8 +81,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
is ChatInfo.ContactRequest ->
ChatListNavLinkLayout(
chatLinkPreview = { ContactRequestView(chat.chatInfo) },
click = { contactRequestAlertDialog(chat.chatInfo, chatModel) },
dropdownMenuItems = { ContactRequestMenuItems(chat.chatInfo, chatModel, showMenu) },
click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) },
dropdownMenuItems = { ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) },
showMenu,
stopped,
selectedChat
@ -94,10 +94,10 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
ModalManager.center.closeModals()
ModalManager.end.closeModals()
ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close ->
ContactConnectionInfoView(chatModel, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close)
ContactConnectionInfoView(chatModel, chat.remoteHostId, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close)
}
},
dropdownMenuItems = { ContactConnectionMenuItems(chat.chatInfo, chatModel, showMenu) },
dropdownMenuItems = { ContactConnectionMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) },
showMenu,
stopped,
selectedChat
@ -119,38 +119,38 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
}
}
fun directChatAction(contact: Contact, chatModel: ChatModel) {
fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
when {
contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, contact, close = null, openChat = true)
else -> withBGApi { openChat(ChatInfo.Direct(contact), chatModel) }
contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
else -> withBGApi { openChat(rhId, ChatInfo.Direct(contact), chatModel) }
}
}
fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
when (groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(groupInfo, chatModel, inProgress)
GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress)
GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert()
else -> withBGApi { openChat(ChatInfo.Group(groupInfo), chatModel) }
else -> withBGApi { openChat(rhId, ChatInfo.Group(groupInfo), chatModel) }
}
}
suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) {
val chat = chatModel.controller.apiGetChat(ChatType.Direct, contactId)
suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) {
val chat = chatModel.controller.apiGetChat(rhId, ChatType.Direct, contactId)
if (chat != null) {
openLoadedChat(chat, chatModel)
}
}
suspend fun openGroupChat(groupId: Long, chatModel: ChatModel) {
val chat = chatModel.controller.apiGetChat(ChatType.Group, groupId)
suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) {
val chat = chatModel.controller.apiGetChat(rhId, ChatType.Group, groupId)
if (chat != null) {
openLoadedChat(chat, chatModel)
}
}
suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) {
Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId)
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId)
if (chat != null) {
openLoadedChat(chat, chatModel)
Log.d(TAG, "TODOCHAT: openChat: opened ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
@ -164,22 +164,24 @@ fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
chatModel.chatId.value = chat.chatInfo.id
}
suspend fun apiLoadPrevMessages(chatInfo: ChatInfo, chatModel: ChatModel, beforeChatItemId: Long, search: String) {
suspend fun apiLoadPrevMessages(ch: Chat, chatModel: ChatModel, beforeChatItemId: Long, search: String) {
val chatInfo = ch.chatInfo
val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT)
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return
if (chatModel.chatId.value != chat.id) return
chatModel.chatItems.addAll(0, chat.chatItems)
}
suspend fun apiFindMessages(chatInfo: ChatInfo, chatModel: ChatModel, search: String) {
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, search = search) ?: return
suspend fun apiFindMessages(ch: Chat, chatModel: ChatModel, search: String) {
val chatInfo = ch.chatInfo
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return
if (chatModel.chatId.value != chat.id) return
chatModel.chatItems.clear()
chatModel.chatItems.addAll(0, chat.chatItems)
}
suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) {
val groupMembers = chatModel.controller.apiListMembers(groupInfo.groupId)
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
val groupMembers = chatModel.controller.apiListMembers(rhId, groupInfo.groupId)
val currentMembers = chatModel.groupMembers
val newMembers = groupMembers.map { newMember ->
val currentMember = currentMembers.find { it.id == newMember.id }
@ -230,7 +232,7 @@ fun GroupMenuItems(
}
GroupMemberStatus.MemAccepted -> {
if (groupInfo.membership.memberCurrent) {
LeaveGroupAction(groupInfo, chatModel, showMenu)
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
}
if (groupInfo.canDelete) {
DeleteGroupAction(chat, groupInfo, chatModel, showMenu)
@ -246,7 +248,7 @@ fun GroupMenuItems(
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
ClearChatAction(chat, chatModel, showMenu)
if (groupInfo.membership.memberCurrent) {
LeaveGroupAction(groupInfo, chatModel, showMenu)
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
}
if (groupInfo.canDelete) {
DeleteGroupAction(chat, groupInfo, chatModel, showMenu)
@ -310,7 +312,7 @@ fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boo
stringResource(MR.strings.clear_chat_menu_action),
painterResource(MR.images.ic_settings_backup_restore),
onClick = {
clearChatDialog(chat.chatInfo, chatModel)
clearChatDialog(chat, chatModel)
showMenu.value = false
},
color = WarningOrange
@ -323,7 +325,7 @@ fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState
stringResource(MR.strings.delete_contact_menu_action),
painterResource(MR.images.ic_delete),
onClick = {
deleteContactDialog(chat.chatInfo, chatModel)
deleteContactDialog(chat, chatModel)
showMenu.value = false
},
color = Color.Red
@ -336,7 +338,7 @@ fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, sh
stringResource(MR.strings.delete_group_menu_action),
painterResource(MR.images.ic_delete),
onClick = {
deleteGroupDialog(chat.chatInfo, groupInfo, chatModel)
deleteGroupDialog(chat, groupInfo, chatModel)
showMenu.value = false
},
color = Color.Red
@ -354,7 +356,7 @@ fun JoinGroupAction(
val joinGroup: () -> Unit = {
withApi {
inProgress.value = true
chatModel.controller.apiJoinGroup(groupInfo.groupId)
chatModel.controller.apiJoinGroup(chat.remoteHostId, groupInfo.groupId)
inProgress.value = false
}
}
@ -370,12 +372,12 @@ fun JoinGroupAction(
}
@Composable
fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
fun LeaveGroupAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction(
stringResource(MR.strings.leave_group_button),
painterResource(MR.images.ic_logout),
onClick = {
leaveGroupDialog(groupInfo, chatModel)
leaveGroupDialog(rhId, groupInfo, chatModel)
showMenu.value = false
},
color = Color.Red
@ -383,13 +385,13 @@ fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: Mutab
}
@Composable
fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction(
stringResource(MR.strings.accept_contact_button),
painterResource(MR.images.ic_check),
color = MaterialTheme.colors.onBackground,
onClick = {
acceptContactRequest(incognito = false, chatInfo.apiId, chatInfo, true, chatModel)
acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel)
showMenu.value = false
}
)
@ -398,7 +400,7 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
painterResource(MR.images.ic_theater_comedy),
color = MaterialTheme.colors.onBackground,
onClick = {
acceptContactRequest(incognito = true, chatInfo.apiId, chatInfo, true, chatModel)
acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel)
showMenu.value = false
}
)
@ -406,7 +408,7 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
stringResource(MR.strings.reject_contact_button),
painterResource(MR.images.ic_close),
onClick = {
rejectContactRequest(chatInfo, chatModel)
rejectContactRequest(rhId, chatInfo, chatModel)
showMenu.value = false
},
color = Color.Red
@ -414,7 +416,7 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
}
@Composable
fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
fun ContactConnectionMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactConnection, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction(
stringResource(MR.strings.set_contact_name),
painterResource(MR.images.ic_edit),
@ -422,7 +424,7 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
ModalManager.center.closeModals()
ModalManager.end.closeModals()
ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close ->
ContactConnectionInfoView(chatModel, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close)
ContactConnectionInfoView(chatModel, rhId, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close)
}
showMenu.value = false
},
@ -431,7 +433,7 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
stringResource(MR.strings.delete_verb),
painterResource(MR.images.ic_delete),
onClick = {
deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {
deleteContactConnectionAlert(rhId, chatInfo.contactConnection, chatModel) {
if (chatModel.chatId.value == null) {
ModalManager.center.closeModals()
ModalManager.end.closeModals()
@ -471,8 +473,9 @@ fun markChatRead(c: Chat, chatModel: ChatModel) {
withApi {
if (chat.chatStats.unreadCount > 0) {
val minUnreadItemId = chat.chatStats.minUnreadItemId
chatModel.markChatItemsRead(chat.chatInfo)
chatModel.markChatItemsRead(chat)
chatModel.controller.apiChatRead(
chat.remoteHostId,
chat.chatInfo.chatType,
chat.chatInfo.apiId,
CC.ItemRange(minUnreadItemId, chat.chatItems.last().id)
@ -481,12 +484,13 @@ fun markChatRead(c: Chat, chatModel: ChatModel) {
}
if (chat.chatStats.unreadChat) {
val success = chatModel.controller.apiChatUnread(
chat.remoteHostId,
chat.chatInfo.chatType,
chat.chatInfo.apiId,
false
)
if (success) {
chatModel.replaceChat(chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
}
}
}
@ -498,17 +502,18 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
withApi {
val success = chatModel.controller.apiChatUnread(
chat.remoteHostId,
chat.chatInfo.chatType,
chat.chatInfo.apiId,
true
)
if (success) {
chatModel.replaceChat(chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
}
}
}
fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.accept_connection_request__question),
text = AnnotatedString(generalGetString(MR.strings.if_you_choose_to_reject_the_sender_will_not_be_notified)),
@ -516,19 +521,19 @@ fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel
Column {
SectionItemView({
AlertManager.shared.hideAlert()
acceptContactRequest(incognito = false, contactRequest.apiId, contactRequest, true, chatModel)
acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel)
}) {
Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
acceptContactRequest(incognito = true, contactRequest.apiId, contactRequest, true, chatModel)
acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel)
}) {
Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
rejectContactRequest(contactRequest, chatModel)
rejectContactRequest(rhId, contactRequest, chatModel)
}) {
Text(generalGetString(MR.strings.reject_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
@ -537,24 +542,24 @@ fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel
)
}
fun acceptContactRequest(incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) {
fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) {
withApi {
val contact = chatModel.controller.apiAcceptContactRequest(incognito, apiId)
val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId)
if (contact != null && isCurrentUser && contactRequest != null) {
val chat = Chat(ChatInfo.Direct(contact), listOf())
chatModel.replaceChat(contactRequest.id, chat)
val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf())
chatModel.replaceChat(rhId, contactRequest.id, chat)
}
}
}
fun rejectContactRequest(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
withApi {
chatModel.controller.apiRejectContactRequest(contactRequest.apiId)
chatModel.removeChat(contactRequest.id)
chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId)
chatModel.removeChat(rhId, contactRequest.id)
}
}
fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) {
fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_pending_connection__question),
text = generalGetString(
@ -565,8 +570,8 @@ fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel
onConfirm = {
withApi {
AlertManager.shared.hideAlert()
if (chatModel.controller.apiDeleteChat(ChatType.ContactConnection, connection.apiId)) {
chatModel.removeChat(connection.id)
if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) {
chatModel.removeChat(rhId, connection.id)
onSuccess()
}
}
@ -575,16 +580,17 @@ fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel
)
}
fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
// TODO why is it not used
fun pendingContactAlertDialog(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.alert_title_contact_connection_pending),
text = generalGetString(MR.strings.alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry),
confirmText = generalGetString(MR.strings.button_delete_contact),
onConfirm = {
withApi {
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId)
if (r) {
chatModel.removeChat(chatInfo.id)
chatModel.removeChat(rhId, chatInfo.id)
if (chatModel.chatId.value == chatInfo.id) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
@ -599,6 +605,7 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
fun askCurrentOrIncognitoProfileConnectContactViaAddress(
chatModel: ChatModel,
rhId: Long?,
contact: Contact,
close: (() -> Unit)?,
openChat: Boolean
@ -611,9 +618,9 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
AlertManager.shared.hideAlert()
withApi {
close?.invoke()
val ok = connectContactViaAddress(chatModel, contact.contactId, incognito = false)
val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false)
if (ok && openChat) {
openDirectChat(contact.contactId, chatModel)
openDirectChat(rhId, contact.contactId, chatModel)
}
}
}) {
@ -623,9 +630,9 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
AlertManager.shared.hideAlert()
withApi {
close?.invoke()
val ok = connectContactViaAddress(chatModel, contact.contactId, incognito = true)
val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true)
if (ok && openChat) {
openDirectChat(contact.contactId, chatModel)
openDirectChat(rhId, contact.contactId, chatModel)
}
}
}) {
@ -641,10 +648,10 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
)
}
suspend fun connectContactViaAddress(chatModel: ChatModel, contactId: Long, incognito: Boolean): Boolean {
val contact = chatModel.controller.apiConnectContactViaAddress(incognito, contactId)
suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactId: Long, incognito: Boolean): Boolean {
val contact = chatModel.controller.apiConnectContactViaAddress(rhId, incognito, contactId)
if (contact != null) {
chatModel.updateContact(contact)
chatModel.updateContact(rhId, contact)
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.connection_request_sent),
text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted)
@ -654,7 +661,7 @@ suspend fun connectContactViaAddress(chatModel: ChatModel, contactId: Long, inco
return false
}
fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
fun acceptGroupInvitationAlertDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.join_group_question),
text = generalGetString(MR.strings.you_are_invited_to_group_join_to_connect_with_group_members),
@ -662,12 +669,12 @@ fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel,
onConfirm = {
withApi {
inProgress?.value = true
chatModel.controller.apiJoinGroup(groupInfo.groupId)
chatModel.controller.apiJoinGroup(rhId, groupInfo.groupId)
inProgress?.value = false
}
},
dismissText = generalGetString(MR.strings.delete_verb),
onDismiss = { deleteGroup(groupInfo, chatModel) }
onDismiss = { deleteGroup(rhId, groupInfo, chatModel) }
)
}
@ -679,11 +686,11 @@ fun cantInviteIncognitoAlert() {
)
}
fun deleteGroup(groupInfo: GroupInfo, chatModel: ChatModel) {
fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
withApi {
val r = chatModel.controller.apiDeleteChat(ChatType.Group, groupInfo.apiId)
val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId)
if (r) {
chatModel.removeChat(groupInfo.id)
chatModel.removeChat(rhId, groupInfo.id)
if (chatModel.chatId.value == groupInfo.id) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
@ -723,15 +730,15 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo
withApi {
val res = when (newChatInfo) {
is ChatInfo.Direct -> with(newChatInfo) {
chatModel.controller.apiSetSettings(chatType, apiId, contact.chatSettings)
chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, contact.chatSettings)
}
is ChatInfo.Group -> with(newChatInfo) {
chatModel.controller.apiSetSettings(chatType, apiId, groupInfo.chatSettings)
chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, groupInfo.chatSettings)
}
else -> false
}
if (res && newChatInfo != null) {
chatModel.updateChatInfo(newChatInfo)
chatModel.updateChatInfo(chat.remoteHostId, newChatInfo)
if (chatSettings.enableNtfs != MsgFilter.All) {
ntfManager.cancelNotificationsForChat(chat.id)
}

View File

@ -53,7 +53,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
val url = chatModel.appOpenUrl.value
if (url != null) {
chatModel.appOpenUrl.value = null
connectIfOpenedViaUri(url, chatModel)
connectIfOpenedViaUri(chatModel.remoteHostId, url, chatModel)
}
}
if (appPlatform.isDesktop) {
@ -117,7 +117,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
}
if (searchInList.isEmpty()) {
DesktopActiveCallOverlayLayout(newChatSheetState)
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
// TODO disable this button and sheet for the duration of the switch
NewChatSheet(chatModel, chatModel.remoteHostId, newChatSheetState, stopped, hideNewChatSheet)
}
if (appPlatform.isAndroid) {
UserPicker(chatModel, userPickerState, switchingUsersAndHosts) {
@ -317,13 +318,13 @@ private fun ProgressIndicator() {
@Composable
expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>)
fun connectIfOpenedViaUri(uri: URI, chatModel: ChatModel) {
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
if (chatModel.currentUser.value == null) {
chatModel.appOpenUrl.value = uri
} else {
withApi {
planAndConnect(chatModel, uri, incognito = null, close = null)
planAndConnect(chatModel, rhId, uri, incognito = null, close = null)
}
}
}

View File

@ -20,13 +20,13 @@ fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) {
is ChatInfo.Direct ->
ShareListNavLinkLayout(
chatLinkPreview = { SharePreviewView(chat) },
click = { directChatAction(chat.chatInfo.contact, chatModel) },
click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) },
stopped
)
is ChatInfo.Group ->
ShareListNavLinkLayout(
chatLinkPreview = { SharePreviewView(chat) },
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
click = { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) },
stopped
)
is ChatInfo.ContactRequest, is ChatInfo.ContactConnection, is ChatInfo.InvalidJSON -> {}

View File

@ -84,7 +84,7 @@ fun UserPicker(
.filter { it }
.collect {
try {
val updatedUsers = chatModel.controller.listUsers().sortedByDescending { it.user.activeUser }
val updatedUsers = chatModel.controller.listUsers(chatModel.remoteHostId).sortedByDescending { it.user.activeUser }
var same = users.size == updatedUsers.size
if (same) {
for (i in 0 until minOf(users.size, updatedUsers.size)) {
@ -129,7 +129,7 @@ fun UserPicker(
switchingUsersAndHosts.value = true
}
ModalManager.closeAllModalsEverywhere()
chatModel.controller.changeActiveUser(u.user.userId, null)
chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null)
job.cancel()
switchingUsersAndHosts.value = false
}

View File

@ -65,6 +65,8 @@ fun DatabaseView(
Box(
Modifier.fillMaxSize(),
) {
val user = m.currentUser.value
val rhId = user?.remoteHostId
DatabaseLayout(
progressIndicator.value,
remember { m.chatRunning }.value != false,
@ -80,7 +82,7 @@ fun DatabaseView(
chatLastStart,
appFilesCountAndSize,
chatItemTTL,
m.currentUser.value,
user,
m.users,
startChat = { startChat(m, chatLastStart, m.chatDbChanged) },
stopChatAlert = { stopChatAlert(m) },
@ -91,9 +93,9 @@ fun DatabaseView(
val oldValue = chatItemTTL.value
chatItemTTL.value = it
if (it < oldValue) {
setChatItemTTLAlert(m, chatItemTTL, progressIndicator, appFilesCountAndSize)
setChatItemTTLAlert(m, rhId, chatItemTTL, progressIndicator, appFilesCountAndSize)
} else if (it != oldValue) {
setCiTTL(m, chatItemTTL, progressIndicator, appFilesCountAndSize)
setCiTTL(m, rhId, chatItemTTL, progressIndicator, appFilesCountAndSize)
}
},
showSettingsModal
@ -265,7 +267,7 @@ fun DatabaseLayout(
}
private fun setChatItemTTLAlert(
m: ChatModel, selectedChatItemTTL: MutableState<ChatItemTTL>,
m: ChatModel, rhId: Long?, selectedChatItemTTL: MutableState<ChatItemTTL>,
progressIndicator: MutableState<Boolean>,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
) {
@ -273,7 +275,7 @@ private fun setChatItemTTLAlert(
title = generalGetString(MR.strings.enable_automatic_deletion_question),
text = generalGetString(MR.strings.enable_automatic_deletion_message),
confirmText = generalGetString(MR.strings.delete_messages),
onConfirm = { setCiTTL(m, selectedChatItemTTL, progressIndicator, appFilesCountAndSize) },
onConfirm = { setCiTTL(m, rhId, selectedChatItemTTL, progressIndicator, appFilesCountAndSize) },
onDismiss = { selectedChatItemTTL.value = m.chatItemTTL.value },
destructive = true,
)
@ -592,6 +594,7 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) {
private fun setCiTTL(
m: ChatModel,
rhId: Long?,
chatItemTTL: MutableState<ChatItemTTL>,
progressIndicator: MutableState<Boolean>,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
@ -600,7 +603,7 @@ private fun setCiTTL(
progressIndicator.value = true
withApi {
try {
m.controller.setChatItemTTL(chatItemTTL.value)
m.controller.setChatItemTTL(rhId, chatItemTTL.value)
// Update model on success
m.chatItemTTL.value = chatItemTTL.value
afterSetCiTTL(m, progressIndicator, appFilesCountAndSize)
@ -623,7 +626,8 @@ private fun afterSetCiTTL(
withApi {
try {
updatingChatsMutex.withLock {
val chats = m.controller.apiGetChats()
// this is using current remote host on purpose - if it changes during update, it will load correct chats
val chats = m.controller.apiGetChats(m.remoteHostId)
m.updateChats(chats)
}
} catch (e: Exception) {

View File

@ -373,7 +373,7 @@ inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
fun UriHandler.openVerifiedSimplexUri(uri: String) {
val URI = try { URI.create(uri) } catch (e: Exception) { null }
if (URI != null) {
connectIfOpenedViaUri(URI, ChatModel)
connectIfOpenedViaUri(chatModel.remoteHostId, URI, ChatModel)
}
}

View File

@ -63,7 +63,7 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
if (!displayName.isNullOrEmpty()) {
profile = Profile(displayName = displayName, fullName = "")
}
val createdUser = m.controller.apiCreateActiveUser(profile, pastTimestamp = true)
val createdUser = m.controller.apiCreateActiveUser(null, profile, pastTimestamp = true)
m.currentUser.value = createdUser
m.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
if (createdUser != null) {

View File

@ -25,11 +25,13 @@ import chat.simplex.res.MR
@Composable
fun AddContactView(
chatModel: ChatModel,
rhId: Long?,
connReqInvitation: String,
contactConnection: MutableState<PendingContactConnection?>
) {
val clipboard = LocalClipboardManager.current
AddContactLayout(
rhId = rhId,
chatModel = chatModel,
incognitoPref = chatModel.controller.appPrefs.incognito,
connReq = connReqInvitation,
@ -52,6 +54,7 @@ fun AddContactView(
@Composable
fun AddContactLayout(
chatModel: ChatModel,
rhId: Long?,
incognitoPref: SharedPreference<Boolean>,
connReq: String,
contactConnection: MutableState<PendingContactConnection?>,
@ -63,9 +66,9 @@ fun AddContactLayout(
withApi {
val contactConnVal = contactConnection.value
if (contactConnVal != null) {
chatModel.controller.apiSetConnectionIncognito(contactConnVal.pccConnId, incognito.value)?.let {
chatModel.controller.apiSetConnectionIncognito(rhId, contactConnVal.pccConnId, incognito.value)?.let {
contactConnection.value = it
chatModel.updateContactConnection(it)
chatModel.updateContactConnection(rhId, it)
}
}
}
@ -172,6 +175,7 @@ fun sharedProfileInfo(
fun PreviewAddContactView() {
SimpleXTheme {
AddContactLayout(
rhId = null,
chatModel = ChatModel,
incognitoPref = SharedPreference({ false }, {}),
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",

View File

@ -32,25 +32,25 @@ import kotlinx.coroutines.launch
import java.net.URI
@Composable
fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
fun AddGroupView(chatModel: ChatModel, rhId: Long?, close: () -> Unit) {
AddGroupLayout(
createGroup = { incognito, groupProfile ->
withApi {
val groupInfo = chatModel.controller.apiNewGroup(incognito, groupProfile)
val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile)
if (groupInfo != null) {
chatModel.addChat(Chat(chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf()))
chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf()))
chatModel.chatItems.clear()
chatModel.chatItemStatuses.clear()
chatModel.chatId.value = groupInfo.id
setGroupMembers(groupInfo, chatModel)
setGroupMembers(rhId, groupInfo, chatModel)
close.invoke()
if (!groupInfo.incognito) {
ModalManager.end.showModalCloseable(true) { close ->
AddGroupMembersView(groupInfo, creatingGroup = true, chatModel, close)
AddGroupMembersView(rhId, groupInfo, creatingGroup = true, chatModel, close)
}
} else {
ModalManager.end.showModalCloseable(true) { close ->
GroupLinkView(chatModel, groupInfo, connReqContact = null, memberRole = null, onGroupLinkUpdated = null, creatingGroup = true, close)
GroupLinkView(chatModel, rhId, groupInfo, connReqContact = null, memberRole = null, onGroupLinkUpdated = null, creatingGroup = true, close)
}
}
}

View File

@ -8,4 +8,4 @@ enum class ConnectViaLinkTab {
}
@Composable
expect fun ConnectViaLinkView(m: ChatModel, close: () -> Unit)
expect fun ConnectViaLinkView(m: ChatModel, rhId: Long?, close: () -> Unit)

View File

@ -30,6 +30,7 @@ import chat.simplex.res.MR
@Composable
fun ContactConnectionInfoView(
chatModel: ChatModel,
rhId: Long?,
connReqInvitation: String?,
contactConnection: PendingContactConnection,
focusAlias: Boolean,
@ -55,8 +56,8 @@ fun ContactConnectionInfoView(
connReq = connReqInvitation,
contactConnection = contactConnection,
focusAlias = focusAlias,
deleteConnection = { deleteContactConnectionAlert(contactConnection, chatModel, close) },
onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) },
deleteConnection = { deleteContactConnectionAlert(rhId, contactConnection, chatModel, close) },
onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) },
share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) },
learnMore = {
ModalManager.center.showModal {
@ -165,9 +166,9 @@ fun DeleteButton(onClick: () -> Unit) {
)
}
private fun setContactAlias(contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withApi {
chatModel.controller.apiSetConnectionAlias(contactConnection.pccConnId, localAlias)?.let {
chatModel.updateContactConnection(it)
private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withApi {
chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let {
chatModel.updateContactConnection(rhId, it)
}
}

View File

@ -20,7 +20,7 @@ enum class CreateLinkTab {
}
@Composable
fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
fun CreateLinkView(m: ChatModel, rhId: Long?, initialSelection: CreateLinkTab) {
val selection = remember { mutableStateOf(initialSelection) }
val connReqInvitation = rememberSaveable { m.connReqInv }
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(null) }
@ -32,7 +32,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
&& contactConnection.value == null
&& !creatingConnReq.value
) {
createInvitation(m, creatingConnReq, connReqInvitation, contactConnection)
createInvitation(m, rhId, creatingConnReq, connReqInvitation, contactConnection)
}
}
/** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv].
@ -65,10 +65,10 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
Column(Modifier.weight(1f)) {
when (selection.value) {
CreateLinkTab.ONE_TIME -> {
AddContactView(m, connReqInvitation.value ?: "", contactConnection)
AddContactView(m, rhId,connReqInvitation.value ?: "", contactConnection)
}
CreateLinkTab.LONG_TERM -> {
UserAddressView(m, viaCreateLinkView = true, close = {})
UserAddressView(m, rhId, viaCreateLinkView = true, close = {})
}
}
}
@ -100,13 +100,14 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
private fun createInvitation(
m: ChatModel,
rhId: Long?,
creatingConnReq: MutableState<Boolean>,
connReqInvitation: MutableState<String?>,
contactConnection: MutableState<PendingContactConnection?>
) {
creatingConnReq.value = true
withApi {
val r = m.controller.apiAddContact(incognito = m.controller.appPrefs.incognito.get())
val r = m.controller.apiAddContact(rhId, incognito = m.controller.appPrefs.incognito.get())
if (r != null) {
connReqInvitation.value = r.first
contactConnection.value = r.second

View File

@ -33,7 +33,8 @@ import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
fun NewChatSheet(chatModel: ChatModel, rhId: Long?, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
// TODO close new chat if remote host changes in model
if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) }
NewChatSheetLayout(
newChatSheetState,
@ -41,17 +42,17 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
addContact = {
closeNewChatSheet(false)
ModalManager.center.closeModals()
ModalManager.center.showModal { CreateLinkView(chatModel, CreateLinkTab.ONE_TIME) }
ModalManager.center.showModal { CreateLinkView(chatModel, rhId, CreateLinkTab.ONE_TIME) }
},
connectViaLink = {
closeNewChatSheet(false)
ModalManager.center.closeModals()
ModalManager.center.showModalCloseable { close -> ConnectViaLinkView(chatModel, close) }
ModalManager.center.showModalCloseable { close -> ConnectViaLinkView(chatModel, rhId, close) }
},
createGroup = {
closeNewChatSheet(false)
ModalManager.center.closeModals()
ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, close) }
ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, rhId, close) }
},
closeNewChatSheet,
)

View File

@ -24,11 +24,12 @@ import chat.simplex.res.MR
import java.net.URI
@Composable
fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
fun PasteToConnectView(chatModel: ChatModel, rhId: Long?, close: () -> Unit) {
val connectionLink = remember { mutableStateOf("") }
val clipboard = LocalClipboardManager.current
PasteToConnectLayout(
chatModel = chatModel,
rhId = rhId,
incognitoPref = chatModel.controller.appPrefs.incognito,
connectionLink = connectionLink,
pasteFromClipboard = {
@ -41,6 +42,7 @@ fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
@Composable
fun PasteToConnectLayout(
chatModel: ChatModel,
rhId: Long?,
incognitoPref: SharedPreference<Boolean>,
connectionLink: MutableState<String>,
pasteFromClipboard: () -> Unit,
@ -52,7 +54,7 @@ fun PasteToConnectLayout(
try {
val uri = URI(connReqUri)
withApi {
planAndConnect(chatModel, uri, incognito = incognito.value, close)
planAndConnect(chatModel, rhId, uri, incognito = incognito.value, close)
}
} catch (e: RuntimeException) {
AlertManager.shared.showAlertMsg(
@ -124,6 +126,7 @@ fun PreviewPasteToConnectTextbox() {
SimpleXTheme {
PasteToConnectLayout(
chatModel = ChatModel,
rhId = null,
incognitoPref = SharedPreference({ false }, {}),
connectionLink = remember { mutableStateOf("") },
pasteFromClipboard = {},

View File

@ -26,7 +26,7 @@ import chat.simplex.res.MR
import java.net.URI
@Composable
expect fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit)
expect fun ScanToConnectView(chatModel: ChatModel, rhId: Long?, close: () -> Unit)
enum class ConnectionLinkType {
INVITATION, CONTACT, GROUP
@ -34,21 +34,22 @@ enum class ConnectionLinkType {
suspend fun planAndConnect(
chatModel: ChatModel,
rhId: Long?,
uri: URI,
incognito: Boolean?,
close: (() -> Unit)?
) {
val connectionPlan = chatModel.controller.apiConnectPlan(uri.toString())
val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri.toString())
if (connectionPlan != null) {
when (connectionPlan) {
is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) {
InvitationLinkPlan.Ok -> {
Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito")
if (incognito != null) {
connectViaUri(chatModel, uri, incognito, connectionPlan, close)
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
chatModel, rhId, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_via_invitation_link),
text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)),
connectDestructive = false
@ -62,12 +63,12 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link),
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true,
)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
chatModel, rhId, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link)),
connectDestructive = true
@ -78,7 +79,7 @@ suspend fun planAndConnect(
Log.d(TAG, "planAndConnect, .InvitationLink, .Connecting, incognito=$incognito")
val contact = connectionPlan.invitationLinkPlan.contact_
if (contact != null) {
openKnownContact(chatModel, close, contact)
openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName)
@ -93,7 +94,7 @@ suspend fun planAndConnect(
is InvitationLinkPlan.Known -> {
Log.d(TAG, "planAndConnect, .InvitationLink, .Known, incognito=$incognito")
val contact = connectionPlan.invitationLinkPlan.contact
openKnownContact(chatModel, close, contact)
openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName)
@ -104,10 +105,10 @@ suspend fun planAndConnect(
ContactAddressPlan.Ok -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito")
if (incognito != null) {
connectViaUri(chatModel, uri, incognito, connectionPlan, close)
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
chatModel, rhId, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_via_contact_link),
text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)),
connectDestructive = false
@ -121,12 +122,12 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address),
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true,
)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
chatModel, rhId, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address)),
connectDestructive = true
@ -140,12 +141,12 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address),
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true,
)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
chatModel, rhId, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address)),
connectDestructive = true
@ -155,7 +156,7 @@ suspend fun planAndConnect(
is ContactAddressPlan.ConnectingProhibit -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit, incognito=$incognito")
val contact = connectionPlan.contactAddressPlan.contact
openKnownContact(chatModel, close, contact)
openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName)
@ -164,7 +165,7 @@ suspend fun planAndConnect(
is ContactAddressPlan.Known -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .Known, incognito=$incognito")
val contact = connectionPlan.contactAddressPlan.contact
openKnownContact(chatModel, close, contact)
openKnownContact(chatModel, rhId, close, contact)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName)
@ -175,9 +176,9 @@ suspend fun planAndConnect(
val contact = connectionPlan.contactAddressPlan.contact
if (incognito != null) {
close?.invoke()
connectContactViaAddress(chatModel, contact.contactId, incognito)
connectContactViaAddress(chatModel, rhId, contact.contactId, incognito)
} else {
askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, contact, close, openChat = false)
askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false)
}
}
}
@ -189,11 +190,11 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_via_group_link),
text = generalGetString(MR.strings.you_will_join_group),
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } }
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }
)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
chatModel, rhId, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_via_group_link),
text = AnnotatedString(generalGetString(MR.strings.you_will_join_group)),
connectDestructive = false
@ -203,7 +204,7 @@ suspend fun planAndConnect(
is GroupLinkPlan.OwnLink -> {
Log.d(TAG, "planAndConnect, .GroupLink, .OwnLink, incognito=$incognito")
val groupInfo = connectionPlan.groupLinkPlan.groupInfo
ownGroupLinkConfirmConnect(chatModel, uri, incognito, connectionPlan, groupInfo, close)
ownGroupLinkConfirmConnect(chatModel, rhId, uri, incognito, connectionPlan, groupInfo, close)
}
GroupLinkPlan.ConnectingConfirmReconnect -> {
Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect, incognito=$incognito")
@ -212,12 +213,12 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_repeat_join_request),
text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link),
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
destructive = true,
)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan, close,
chatModel, rhId, uri, connectionPlan, close,
title = generalGetString(MR.strings.connect_plan_repeat_join_request),
text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link)),
connectDestructive = true
@ -242,7 +243,7 @@ suspend fun planAndConnect(
is GroupLinkPlan.Known -> {
Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito")
val groupInfo = connectionPlan.groupLinkPlan.groupInfo
openKnownGroup(chatModel, close, groupInfo)
openKnownGroup(chatModel, rhId, close, groupInfo)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.connect_plan_group_already_exists),
String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName)
@ -253,10 +254,10 @@ suspend fun planAndConnect(
} else {
Log.d(TAG, "planAndConnect, plan error")
if (incognito != null) {
connectViaUri(chatModel, uri, incognito, connectionPlan = null, close)
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan = null, close)
} else {
askCurrentOrIncognitoProfileAlert(
chatModel, uri, connectionPlan = null, close,
chatModel, rhId, uri, connectionPlan = null, close,
title = generalGetString(MR.strings.connect_plan_connect_via_link),
connectDestructive = false
)
@ -266,12 +267,13 @@ suspend fun planAndConnect(
suspend fun connectViaUri(
chatModel: ChatModel,
rhId: Long?,
uri: URI,
incognito: Boolean,
connectionPlan: ConnectionPlan?,
close: (() -> Unit)?
): Boolean {
val r = chatModel.controller.apiConnect(incognito, uri.toString())
val r = chatModel.controller.apiConnect(rhId, incognito, uri.toString())
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
if (r) {
close?.invoke()
@ -298,6 +300,7 @@ fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType
fun askCurrentOrIncognitoProfileAlert(
chatModel: ChatModel,
rhId: Long?,
uri: URI,
connectionPlan: ConnectionPlan?,
close: (() -> Unit)?,
@ -314,7 +317,7 @@ fun askCurrentOrIncognitoProfileAlert(
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
connectViaUri(chatModel, uri, incognito = false, connectionPlan, close)
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close)
}
}) {
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor)
@ -322,7 +325,7 @@ fun askCurrentOrIncognitoProfileAlert(
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
connectViaUri(chatModel, uri, incognito = true, connectionPlan, close)
connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close)
}
}) {
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor)
@ -337,18 +340,19 @@ fun askCurrentOrIncognitoProfileAlert(
)
}
fun openKnownContact(chatModel: ChatModel, close: (() -> Unit)?, contact: Contact) {
fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, contact: Contact) {
withApi {
val c = chatModel.getContactChat(contact.contactId)
if (c != null) {
close?.invoke()
openDirectChat(contact.contactId, chatModel)
openDirectChat(rhId, contact.contactId, chatModel)
}
}
}
fun ownGroupLinkConfirmConnect(
chatModel: ChatModel,
rhId: Long?,
uri: URI,
incognito: Boolean?,
connectionPlan: ConnectionPlan?,
@ -363,7 +367,7 @@ fun ownGroupLinkConfirmConnect(
// Open group
SectionItemView({
AlertManager.shared.hideAlert()
openKnownGroup(chatModel, close, groupInfo)
openKnownGroup(chatModel, rhId, close, groupInfo)
}) {
Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
@ -372,7 +376,7 @@ fun ownGroupLinkConfirmConnect(
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
connectViaUri(chatModel, uri, incognito, connectionPlan, close)
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close)
}
}) {
Text(
@ -385,7 +389,7 @@ fun ownGroupLinkConfirmConnect(
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
connectViaUri(chatModel, uri, incognito = false, connectionPlan, close)
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close)
}
}) {
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
@ -394,7 +398,7 @@ fun ownGroupLinkConfirmConnect(
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
connectViaUri(chatModel, uri, incognito = true, connectionPlan, close)
connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close)
}
}) {
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
@ -411,12 +415,12 @@ fun ownGroupLinkConfirmConnect(
)
}
fun openKnownGroup(chatModel: ChatModel, close: (() -> Unit)?, groupInfo: GroupInfo) {
fun openKnownGroup(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, groupInfo: GroupInfo) {
withApi {
val g = chatModel.getGroupChat(groupInfo.groupId)
if (g != null) {
close?.invoke()
openGroupChat(groupInfo.groupId, chatModel)
openGroupChat(rhId, groupInfo.groupId, chatModel)
}
}
}
@ -424,6 +428,7 @@ fun openKnownGroup(chatModel: ChatModel, close: (() -> Unit)?, groupInfo: GroupI
@Composable
fun ConnectContactLayout(
chatModel: ChatModel,
rhId: Long?,
incognitoPref: SharedPreference<Boolean>,
close: () -> Unit
) {
@ -435,7 +440,7 @@ fun ConnectContactLayout(
try {
val uri = URI(connReqUri)
withApi {
planAndConnect(chatModel, uri, incognito = incognito.value, close)
planAndConnect(chatModel, rhId, uri, incognito = incognito.value, close)
}
} catch (e: RuntimeException) {
AlertManager.shared.showAlertMsg(
@ -487,6 +492,7 @@ fun PreviewConnectContactLayout() {
SimpleXTheme {
ConnectContactLayout(
chatModel = ChatModel,
rhId = null,
incognitoPref = SharedPreference({ false }, {}),
close = {},
)

View File

@ -23,14 +23,14 @@ import chat.simplex.common.views.newchat.simplexChatLink
import chat.simplex.res.MR
@Composable
fun CreateSimpleXAddress(m: ChatModel) {
fun CreateSimpleXAddress(m: ChatModel, rhId: Long?) {
var progressIndicator by remember { mutableStateOf(false) }
val userAddress = remember { m.userAddress }
val clipboard = LocalClipboardManager.current
val uriHandler = LocalUriHandler.current
LaunchedEffect(Unit) {
prepareChatBeforeAddressCreation()
prepareChatBeforeAddressCreation(rhId)
}
CreateSimpleXAddressLayout(
@ -45,11 +45,11 @@ fun CreateSimpleXAddress(m: ChatModel) {
createAddress = {
withApi {
progressIndicator = true
val connReqContact = m.controller.apiCreateUserAddress()
val connReqContact = m.controller.apiCreateUserAddress(rhId)
if (connReqContact != null) {
m.userAddress.value = UserContactLinkRec(connReqContact)
try {
val u = m.controller.apiSetProfileAddress(true)
val u = m.controller.apiSetProfileAddress(rhId, true)
if (u != null) {
m.updateUser(u)
}
@ -176,18 +176,18 @@ private fun ProgressIndicator() {
}
}
private fun prepareChatBeforeAddressCreation() {
private fun prepareChatBeforeAddressCreation(rhId: Long?) {
if (chatModel.users.isNotEmpty()) return
withApi {
val user = chatModel.controller.apiGetActiveUser() ?: return@withApi
val user = chatModel.controller.apiGetActiveUser(rhId) ?: return@withApi
chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user)
} else {
val users = chatModel.controller.listUsers()
val users = chatModel.controller.listUsers(rhId)
chatModel.users.clear()
chatModel.users.addAll(users)
chatModel.controller.getUserChatData()
chatModel.controller.getUserChatData(rhId)
}
}
}

View File

@ -80,7 +80,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
onDispose {
if (m.chatRunning.value != true) {
withBGApi {
val user = chatController.apiGetActiveUser()
val user = chatController.apiGetActiveUser(null)
if (user != null) {
m.controller.startChat(user)
}

View File

@ -34,7 +34,7 @@ fun HiddenProfileView(
saveProfilePassword = { hidePassword ->
withBGApi {
try {
val u = m.controller.apiHideUser(user.userId, hidePassword)
val u = m.controller.apiHideUser(user, hidePassword)
m.updateUser(u)
close()
} catch (e: Exception) {

View File

@ -168,9 +168,9 @@ fun NetworkAndServersView(
) {
AppBarTitle(stringResource(MR.strings.network_and_servers))
SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.SMP, close) })
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) })
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.XFTP, close) })
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) })
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal)
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)

View File

@ -25,11 +25,11 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
fun savePrefs(afterSave: () -> Unit = {}) {
withApi {
val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences())
val updated = m.controller.apiUpdateProfile(newProfile)
val updated = m.controller.apiUpdateProfile(user.remoteHostId, newProfile)
if (updated != null) {
val (updatedProfile, updatedContacts) = updated
m.updateCurrentUser(updatedProfile, preferences)
updatedContacts.forEach(m::updateContact)
m.updateCurrentUser(user.remoteHostId, updatedProfile, preferences)
updatedContacts.forEach { m.updateContact(user.remoteHostId, it) }
currentPreferences = preferences
}
afterSave()

View File

@ -99,7 +99,7 @@ fun PrivacySettingsView(
fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) {
withApi {
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserContactReceipts(currentUser.userId, mrs)
chatModel.controller.apiSetUserContactReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
chatModel.currentUser.value = currentUser.copy(sendRcptsContacts = enable)
if (clearOverrides) {
@ -111,7 +111,7 @@ fun PrivacySettingsView(
val sendRcpts = contact.chatSettings.sendRcpts
if (sendRcpts != null && sendRcpts != enable) {
contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null))
chatModel.updateContact(contact)
chatModel.updateContact(currentUser.remoteHostId, contact)
}
}
}
@ -122,7 +122,7 @@ fun PrivacySettingsView(
fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) {
withApi {
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserGroupReceipts(currentUser.userId, mrs)
chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable)
if (clearOverrides) {
@ -134,7 +134,7 @@ fun PrivacySettingsView(
val sendRcpts = groupInfo.chatSettings.sendRcpts
if (sendRcpts != null && sendRcpts != enable) {
groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null))
chatModel.updateGroup(groupInfo)
chatModel.updateGroup(currentUser.remoteHostId, groupInfo)
}
}
}

View File

@ -197,7 +197,7 @@ fun ShowTestStatus(server: ServerCfg, modifier: Modifier = Modifier) =
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, ProtocolTestFailure?> =
try {
val r = m.controller.testProtoServer(server.server)
val r = m.controller.testProtoServer(server.remoteHostId, server.server)
server.copy(tested = r == null) to r
} catch (e: Exception) {
Log.e(TAG, "testServerConnection ${e.stackTraceToString()}")

View File

@ -28,7 +28,8 @@ import chat.simplex.res.MR
import kotlinx.coroutines.launch
@Composable
fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: () -> Unit) {
fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) {
// TODO close if remote host changes
var presetServers by remember { mutableStateOf(emptyList<String>()) }
var servers by remember {
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
@ -51,7 +52,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
}
LaunchedEffect(Unit) {
val res = m.controller.getUserProtoServers(serverProtocol)
val res = m.controller.getUserProtoServers(rhId, serverProtocol)
if (res != null) {
currServers.value = res.protoServers
presetServers = res.presetServers
@ -90,7 +91,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
ModalView(
close = {
if (saveDisabled.value) close()
else showUnsavedChangesAlert({ saveServers(serverProtocol, currServers, servers, m, close) }, close)
else showUnsavedChangesAlert({ saveServers(rhId, serverProtocol, currServers, servers, m, close) }, close)
},
) {
ProtocolServersLayout(
@ -118,7 +119,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
SectionItemView({
AlertManager.shared.hideAlert()
ModalManager.start.showModalCloseable { close ->
ScanProtocolServer {
ScanProtocolServer(rhId) {
close()
servers = servers + it
m.userSMPServersUnsaved.value = servers
@ -133,7 +134,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
if (!hasAllPresets) {
SectionItemView({
AlertManager.shared.hideAlert()
servers = (servers + addAllPresets(presetServers, servers, m)).sortedByDescending { it.preset }
servers = (servers + addAllPresets(rhId, presetServers, servers, m)).sortedByDescending { it.preset }
}) {
Text(stringResource(MR.strings.smp_servers_preset_add), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
@ -155,7 +156,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
m.userSMPServersUnsaved.value = null
},
saveSMPServers = {
saveServers(serverProtocol, currServers, servers, m)
saveServers(rhId, serverProtocol, currServers, servers, m)
},
showServer = ::showServer,
)
@ -289,11 +290,11 @@ private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List<Se
private fun hasAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): Boolean =
presetServers.all { hasPreset(it, servers) } ?: true
private fun addAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
private fun addAllPresets(rhId: Long?, presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
val toAdd = ArrayList<ServerCfg>()
for (srv in presetServers) {
if (!hasPreset(srv, servers)) {
toAdd.add(ServerCfg(srv, preset = true, tested = null, enabled = true))
toAdd.add(ServerCfg(remoteHostId = rhId, srv, preset = true, tested = null, enabled = true))
}
}
return toAdd
@ -346,9 +347,9 @@ private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpd
return fs
}
private fun saveServers(protocol: ServerProtocol, currServers: MutableState<List<ServerCfg>>, servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: MutableState<List<ServerCfg>>, servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
withApi {
if (m.controller.setUserProtoServers(protocol, servers)) {
if (m.controller.setUserProtoServers(rhId, protocol, servers)) {
currServers.value = servers
m.userSMPServersUnsaved.value = null
}

View File

@ -13,10 +13,10 @@ import chat.simplex.common.views.newchat.QRCodeScanner
import chat.simplex.res.MR
@Composable
expect fun ScanProtocolServer(onNext: (ServerCfg) -> Unit)
expect fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit)
@Composable
fun ScanProtocolServerLayout(onNext: (ServerCfg) -> Unit) {
fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) {
Column(
Modifier
.fillMaxSize()
@ -32,7 +32,7 @@ fun ScanProtocolServerLayout(onNext: (ServerCfg) -> Unit) {
QRCodeScanner { text ->
val res = parseServerAddress(text)
if (res != null) {
onNext(ServerCfg(text, false, null, true))
onNext(ServerCfg(remoteHostId = rhId, text, false, null, true))
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.smp_servers_invalid_address),

View File

@ -26,12 +26,12 @@ fun SetDeliveryReceiptsView(m: ChatModel) {
if (currentUser != null) {
withApi {
try {
m.controller.apiSetAllContactReceipts(enable = true)
m.controller.apiSetAllContactReceipts(currentUser.remoteHostId, enable = true)
m.currentUser.value = currentUser.copy(sendRcptsContacts = true)
m.setDeliveryReceipts.value = false
m.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
try {
val users = m.controller.listUsers()
val users = m.controller.listUsers(currentUser.remoteHostId)
m.users.clear()
m.users.addAll(users)
} catch (e: Exception) {

View File

@ -155,7 +155,7 @@ fun SettingsLayout(
}
val profileHidden = rememberSaveable { mutableStateOf(false) }
SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, it.currentUser.value?.remoteHostId, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true)
ChatPreferencesItem(showCustomModal, stopped = stopped)
if (appPlatform.isDesktop) {
SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView(it) }, disabled = stopped, extraPadding = true)

View File

@ -33,10 +33,12 @@ import chat.simplex.res.MR
@Composable
fun UserAddressView(
chatModel: ChatModel,
rhId: Long?,
viaCreateLinkView: Boolean = false,
shareViaProfile: Boolean = false,
close: () -> Unit
) {
// TODO close when remote host changes
val shareViaProfile = remember { mutableStateOf(shareViaProfile) }
var progressIndicator by remember { mutableStateOf(false) }
val onCloseHandler: MutableState<(close: () -> Unit) -> Unit> = remember { mutableStateOf({ _ -> }) }
@ -45,7 +47,7 @@ fun UserAddressView(
progressIndicator = true
withBGApi {
try {
val u = chatModel.controller.apiSetProfileAddress(on)
val u = chatModel.controller.apiSetProfileAddress(rhId, on)
if (u != null) {
chatModel.updateUser(u)
}
@ -67,7 +69,7 @@ fun UserAddressView(
createAddress = {
withApi {
progressIndicator = true
val connReqContact = chatModel.controller.apiCreateUserAddress()
val connReqContact = chatModel.controller.apiCreateUserAddress(rhId)
if (connReqContact != null) {
chatModel.userAddress.value = UserContactLinkRec(connReqContact)
@ -112,7 +114,7 @@ fun UserAddressView(
onConfirm = {
progressIndicator = true
withApi {
val u = chatModel.controller.apiDeleteUserAddress()
val u = chatModel.controller.apiDeleteUserAddress(rhId)
if (u != null) {
chatModel.userAddress.value = null
chatModel.updateUser(u)
@ -126,7 +128,7 @@ fun UserAddressView(
},
saveAas = { aas: AutoAcceptState, savedAAS: MutableState<AutoAcceptState> ->
withBGApi {
val address = chatModel.controller.userAddressAutoAccept(aas.autoAccept)
val address = chatModel.controller.userAddressAutoAccept(rhId, aas.autoAccept)
if (address != null) {
chatModel.userAddress.value = address
savedAAS.value = aas

View File

@ -37,10 +37,10 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
close,
saveProfile = { displayName, fullName, image ->
withApi {
val updated = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName.trim(), fullName = fullName, image = image))
val updated = chatModel.controller.apiUpdateProfile(user.remoteHostId, profile.copy(displayName = displayName.trim(), fullName = fullName, image = image))
if (updated != null) {
val (newProfile, _) = updated
chatModel.updateCurrentUser(newProfile)
chatModel.updateCurrentUser(user.remoteHostId, newProfile)
profile = newProfile
close()
}

View File

@ -57,7 +57,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
ModalManager.end.closeModals()
}
withBGApi {
m.controller.changeActiveUser(user.userId, userViewPassword(user, searchTextOrPassword.value.trim()))
m.controller.changeActiveUser(user.remoteHostId, user.userId, userViewPassword(user, searchTextOrPassword.value.trim()))
}
},
removeUser = { user ->
@ -106,24 +106,24 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
ModalManager.start.showModalCloseable(true) { close ->
ProfileActionView(UserProfileAction.UNHIDE, user) { pwd ->
withBGApi {
setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, pwd) }
setUserPrivacy(m) { m.controller.apiUnhideUser(user, pwd) }
close()
}
}
}
} else {
withBGApi { setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, searchTextOrPassword.value.trim()) } }
withBGApi { setUserPrivacy(m) { m.controller.apiUnhideUser(user, searchTextOrPassword.value.trim()) } }
}
},
muteUser = { user ->
withBGApi {
setUserPrivacy(m, onSuccess = {
if (m.controller.appPrefs.showMuteProfileAlert.get()) showMuteProfileAlert(m.controller.appPrefs.showMuteProfileAlert)
}) { m.controller.apiMuteUser(user.userId) }
}) { m.controller.apiMuteUser(user) }
}
},
unmuteUser = { user ->
withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user.userId) } }
withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user) } }
},
showHiddenProfile = { user ->
ModalManager.start.showModalCloseable(true) { close ->
@ -348,14 +348,14 @@ private suspend fun doRemoveUser(m: ChatModel, user: User, users: List<User>, de
if (users.size < 2) return
suspend fun deleteUser(user: User) {
m.controller.apiDeleteUser(user.userId, delSMPQueues, viewPwd)
m.controller.apiDeleteUser(user, delSMPQueues, viewPwd)
m.removeUser(user)
}
try {
if (user.activeUser) {
val newActive = users.firstOrNull { u -> !u.activeUser && !u.hidden }
if (newActive != null) {
m.controller.changeActiveUser_(newActive.userId, null)
m.controller.changeActiveUser_(newActive.remoteHostId, newActive.userId, null)
deleteUser(user.copy(activeUser = false))
}
} else {

View File

@ -50,22 +50,23 @@ actual fun ActiveCallView() {
val call = chatModel.activeCall.value
if (call != null) {
Log.d(TAG, "has active call $call")
val callRh = call.remoteHostId
when (val r = apiMsg.resp) {
is WCallResponse.Capabilities -> withBGApi {
val callType = CallType(call.localMedia, r.capabilities)
chatModel.controller.apiSendCallInvitation(call.contact, callType)
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
}
is WCallResponse.Offer -> withBGApi {
chatModel.controller.apiSendCallOffer(call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
}
is WCallResponse.Answer -> withBGApi {
chatModel.controller.apiSendCallAnswer(call.contact, r.answer, r.iceCandidates)
chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates)
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
}
is WCallResponse.Ice -> withBGApi {
chatModel.controller.apiSendCallExtraInfo(call.contact, r.iceCandidates)
chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates)
}
is WCallResponse.Connection ->
try {
@ -73,7 +74,7 @@ actual fun ActiveCallView() {
if (callStatus == WebRTCCallStatus.Connected) {
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectedAt = Clock.System.now())
}
withBGApi { chatModel.controller.apiCallStatus(call.contact, callStatus) }
withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) }
} catch (e: Error) {
Log.d(TAG, "call status ${r.state.connectionState} not used")
}

View File

@ -44,7 +44,7 @@ actual fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<An
val chat = chatModel.getChat(call.contact.id)
if (chat != null) {
withApi {
openChat(chat.chatInfo, chatModel)
openChat(chat.remoteHostId, chat.chatInfo, chatModel)
}
}
},

View File

@ -4,6 +4,7 @@ import androidx.compose.runtime.*
import chat.simplex.common.model.ChatModel
@Composable
actual fun ConnectViaLinkView(m: ChatModel, close: () -> Unit) {
PasteToConnectView(m, close)
actual fun ConnectViaLinkView(m: ChatModel, rhId: Long?, close: () -> Unit) {
// TODO this should close if remote host changes in model
PasteToConnectView(m, rhId, close)
}

View File

@ -4,9 +4,10 @@ import androidx.compose.runtime.Composable
import chat.simplex.common.model.ChatModel
@Composable
actual fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
actual fun ScanToConnectView(chatModel: ChatModel, rhId: Long?, close: () -> Unit) {
ConnectContactLayout(
chatModel = chatModel,
rhId = rhId,
incognitoPref = chatModel.controller.appPrefs.incognito,
close = close
)

View File

@ -4,6 +4,6 @@ import androidx.compose.runtime.Composable
import chat.simplex.common.model.ServerCfg
@Composable
actual fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) {
ScanProtocolServerLayout(onNext)
actual fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) {
ScanProtocolServerLayout(rhId, onNext)
}