Compare commits

..

26 Commits

Author SHA1 Message Date
Levitating Pineapple 6ab5e7d02f tail toggle 2024-08-24 20:05:41 +03:00
Levitating Pineapple 29cc2cfb0c decryption error view padding 2024-08-24 17:30:49 +03:00
Levitating Pineapple 4826da0825 rename 2024-08-24 16:47:19 +03:00
Levitating Pineapple 4f9f2cecec ios: add tails and roundness slider to message bubbles 2024-08-24 16:31:44 +03:00
Levitating Pineapple a5c1be3b8f Merge branch 'master' into lp/time-grouped-messages 2024-08-23 19:07:05 +03:00
Levitating Pineapple f7cac769df cleanup 2024-08-23 17:19:22 +03:00
Levitating Pineapple a4b086ce7a cleanup 2024-08-23 17:18:30 +03:00
Levitating Pineapple 2a20afeda6 simplify timestamp logic; remove shape 2024-08-23 17:15:02 +03:00
Evgeny Poberezkin 7b48c59f9f ios: update core library 2024-08-23 14:32:16 +01:00
Diogo 04033fc0b5 ios: connection profile search, incognito info in selection list and improved loader (#4744)
* remove comment

* improve switching chat profile loader

* add search on profile selection

* disable auto correction

* add incognito info in select chat profile

* fix typos

* layout

* fix choosing hidden user

* opacity back

* Revert "layout"

This reverts commit 10f1e5e924.

* remove padding

* selected profile on top (profile or incognito)

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-08-23 13:20:07 +01:00
Evgeny Poberezkin 7b90e01b3a core: 6.1.0.0 2024-08-23 13:18:51 +01:00
Levitating Pineapple beefb1ba12 fix groups 2024-08-23 12:32:24 +03:00
Levitating Pineapple 6c1a821304 cleanup 2024-08-23 11:49:37 +03:00
Levitating Pineapple ec40c2d7e5 add date logic 2024-08-23 11:40:11 +03:00
Levitating Pineapple d6666b270f time separation struct 2024-08-23 11:20:31 +03:00
Levitating Pineapple 950b87b48f chat dir 2024-08-23 09:49:29 +03:00
Levitating Pineapple ebbafc12d4 separate by minute 2024-08-22 23:03:00 +03:00
Levitating Pineapple 8053d8fc39 round to minute 2024-08-22 22:42:29 +03:00
Levitating Pineapple d085a047fc fix chat item not getting updated 2024-08-22 22:32:01 +03:00
spaced4ndy ef1897f865 multiplatform: multi send & forward api (#4745) 2024-08-22 21:39:13 +04:00
spaced4ndy f587179045 ios: multi send & forward api (#4739) 2024-08-22 21:38:22 +04:00
spaced4ndy 791489e943 core: multi forward api (#4704) 2024-08-22 21:36:35 +04:00
Levitating Pineapple 99d5a183c4 hide timestamp 2024-08-22 18:24:53 +03:00
Diogo c485837910 ios: allow for chat profile selection on new chat screen (#4729)
* ios: allow for chat profile selection on new chat screen

* add api and types

* initial api connection with error handling

* improve incognito handling

* adjustments to different server connections

* loading state

* simpler handling of race

* smaller delay

* improve error handling and messages

* fix header

* remove tap section footer

* incognito adjustments

* set UI driving vars in main thread

* remove result

* incognito in profile picker and footer

* put incognito mask inside a circle

* fix click on incognito when already selected

* fix avoid users swapping position when picker is active

* fix pending contact cleanup logic

* icons

* restore incognito help

* fix updating qr code

* remove info from footer

* layout

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-08-22 15:02:32 +01:00
Levitating Pineapple 8979306e68 cleanup 2024-08-22 16:37:54 +03:00
Levitating Pineapple e5a60b963e ios: time based message grouping 2024-08-22 16:31:54 +03:00
107 changed files with 1900 additions and 1493 deletions
+16 -10
View File
@@ -123,6 +123,14 @@ class NetworkModel: ObservableObject {
}
}
/// ChatItemWithMenu can depend on previous or next item for it's appearance
/// This dummy model is used to force an update of all chat items,
/// when they might have changed appearance.
class ChatItemDummyModel: ObservableObject {
static let shared = ChatItemDummyModel()
func sendUpdate() { objectWillChange.send() }
}
final class ChatModel: ObservableObject {
@Published var onboardingStage: OnboardingStage?
@Published var setDeliveryReceipts = false
@@ -428,19 +436,17 @@ final class ChatModel: ObservableObject {
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
if let i = getChatItemIndex(cItem) {
withConditionalAnimation {
_updateChatItem(at: i, with: cItem)
}
_updateChatItem(at: i, with: cItem)
ChatItemDummyModel.shared.sendUpdate()
return false
} else {
withConditionalAnimation(itemAnimation()) {
var ci = cItem
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
ci.meta.itemStatus = status
}
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
im.itemAdded = true
var ci = cItem
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
ci.meta.itemStatus = status
}
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
im.itemAdded = true
ChatItemDummyModel.shared.sendUpdate()
return true
}
+56 -42
View File
@@ -357,17 +357,17 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
throw r
}
func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?) async -> ChatItem? {
let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId, ttl: ttl)
func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? {
let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemIds: itemIds, ttl: ttl)
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
}
func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? {
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
func apiSendMessages(type: ChatType, id: Int64, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
let cmd: ChatCommand = .apiSendMessages(type: type, id: id, live: live, ttl: ttl, composedMessages: composedMessages)
return await processSendMessageCmd(toChatType: type, cmd: cmd)
}
private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> ChatItem? {
private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> [ChatItem]? {
let chatModel = ChatModel.shared
let r: ChatResponse
if toChatType == .direct {
@@ -380,10 +380,13 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async
}
})
r = await chatSendCmd(cmd, bgTask: false)
if case let .newChatItem(_, aChatItem) = r {
cItem = aChatItem.chatItem
chatModel.messageDelivery[aChatItem.chatItem.id] = endTask
return cItem
if case let .newChatItems(_, aChatItems) = r {
let cItems = aChatItems.map { $0.chatItem }
if let cItemLast = cItems.last {
cItem = cItemLast
chatModel.messageDelivery[cItemLast.id] = endTask
}
return cItems
}
if let networkErrorAlert = networkErrorAlert(r) {
AlertManager.shared.showAlert(networkErrorAlert)
@@ -394,18 +397,18 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async
return nil
} else {
r = await chatSendCmd(cmd, bgDelay: msgDelay)
if case let .newChatItem(_, aChatItem) = r {
return aChatItem.chatItem
if case let .newChatItems(_, aChatItems) = r {
return aChatItems.map { $0.chatItem }
}
sendMessageErrorAlert(r)
return nil
}
}
func apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) async -> ChatItem? {
let r = await chatSendCmd(.apiCreateChatItem(noteFolderId: noteFolderId, file: file, msg: msg))
if case let .newChatItem(_, aChatItem) = r { return aChatItem.chatItem }
createChatItemErrorAlert(r)
func apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
let r = await chatSendCmd(.apiCreateChatItems(noteFolderId: noteFolderId, composedMessages: composedMessages))
if case let .newChatItems(_, aChatItems) = r { return aChatItems.map { $0.chatItem } }
createChatItemsErrorAlert(r)
return nil
}
@@ -417,8 +420,8 @@ private func sendMessageErrorAlert(_ r: ChatResponse) {
)
}
private func createChatItemErrorAlert(_ r: ChatResponse) {
logger.error("apiCreateChatItem error: \(String(describing: r))")
private func createChatItemsErrorAlert(_ r: ChatResponse) {
logger.error("apiCreateChatItems error: \(String(describing: r))")
AlertManager.shared.showAlertMsg(
title: "Error creating message",
message: "Error: \(responseError(r))"
@@ -582,13 +585,13 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (Gro
throw r
}
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) {
func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
let r = await chatSendCmd(.apiContactQueueInfo(contactId: contactId))
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
throw r
}
func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) {
func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) {
let r = await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) }
throw r
@@ -673,6 +676,13 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P
throw r
}
func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection? {
let r = await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId))
if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection}
throw r
}
func apiConnectPlan(connReq: String) async throws -> ConnectionPlan {
let userId = try currentUserId("apiConnectPlan")
let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq))
@@ -1775,23 +1785,25 @@ func processReceivedMsg(_ res: ChatResponse) async {
n.networkStatuses = ns
}
}
case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
await MainActor.run {
if active(user) {
m.addChatItem(cInfo, cItem)
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
m.increaseUnreadCounter(user: user)
case let .newChatItems(user, chatItems):
for chatItem in chatItems {
let cInfo = chatItem.chatInfo
let cItem = chatItem.chatItem
await MainActor.run {
if active(user) {
m.addChatItem(cInfo, cItem)
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
m.increaseUnreadCounter(user: user)
}
}
}
if let file = cItem.autoReceiveFile() {
Task {
await receiveFile(user: user, fileId: file.fileId, auto: true)
if let file = cItem.autoReceiveFile() {
Task {
await receiveFile(user: user, fileId: file.fileId, auto: true)
}
}
if cItem.showNotification {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
}
if cItem.showNotification {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
case let .chatItemStatusUpdated(user, aChatItem):
let cInfo = aChatItem.chatInfo
@@ -1801,10 +1813,15 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
case .sndNew: ()
case .sndSent: endTask()
case .sndRcvd: endTask()
case .sndErrorAuth: endTask()
case .sndError: endTask()
default: ()
case .sndWarning: endTask()
case .rcvNew: ()
case .rcvRead: ()
case .invalid: ()
}
}
case let .chatItemUpdated(user, aChatItem):
@@ -2182,11 +2199,9 @@ func refreshCallInvitations() async throws {
}
}
func justRefreshCallInvitations() async throws {
func justRefreshCallInvitations() throws {
let callInvitations = try apiGetCallInvitationsSync()
await MainActor.run {
ChatModel.shared.callInvitations = callsByChat(callInvitations)
}
ChatModel.shared.callInvitations = callsByChat(callInvitations)
}
private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: RcvCallInvitation] {
@@ -2196,13 +2211,12 @@ private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: Rcv
}
func activateCall(_ callInvitation: RcvCallInvitation) {
if !callInvitation.user.showNotifications { return }
let m = ChatModel.shared
logger.debug("reportNewIncomingCall activeCallUUID \(String(describing: m.activeCall?.callUUID)) invitationUUID \(String(describing: callInvitation.callUUID))")
if !callInvitation.user.showNotifications || m.activeCall?.callUUID == callInvitation.callUUID { return }
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
if let error = error {
DispatchQueue.main.async {
m.callInvitations[callInvitation.contact.id]?.callUUID = nil
m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil
}
logger.error("reportNewIncomingCall error: \(error.localizedDescription)")
} else {
@@ -185,7 +185,7 @@ struct ActiveCallView: View {
case .ended:
closeCallView(client)
call.callState = .ended
if let uuid = call.callUUID {
if let uuid = call.callkitUUID {
CallController.shared.endCall(callUUID: uuid)
}
case .ok:
@@ -382,7 +382,7 @@ struct ActiveCallOverlay: View {
private func endCallButton() -> some View {
let cc = CallController.shared
return callButton("phone.down.fill", width: 60, height: 60) {
if let uuid = call.callUUID {
if let uuid = call.callkitUUID {
cc.endCall(callUUID: uuid)
} else {
cc.endCall(call: call) {}
@@ -462,9 +462,9 @@ struct ActiveCallOverlay: View {
struct ActiveCallOverlay_Previews: PreviewProvider {
static var previews: some View {
Group{
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
.background(.black)
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
.background(.black)
}
}
+44 -96
View File
@@ -51,7 +51,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
logger.debug("CallController.provider CXStartCallAction")
if callManager.startOutgoingCall(callUUID: action.callUUID.uuidString.lowercased()) {
if callManager.startOutgoingCall(callUUID: action.callUUID) {
action.fulfill()
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
} else {
@@ -61,30 +61,12 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
logger.debug("CallController.provider CXAnswerCallAction")
Task {
let chatIsReady = await waitUntilChatStarted(timeoutMs: 30_000, stepMs: 500)
logger.debug("CallController chat started \(chatIsReady) \(ChatModel.shared.chatInitialized) \(ChatModel.shared.chatRunning == true) \(String(describing: AppChatState.shared.value))")
if !chatIsReady {
action.fail()
return
}
if !ChatModel.shared.callInvitations.values.contains(where: { inv in inv.callUUID == action.callUUID.uuidString.lowercased() }) {
try? await justRefreshCallInvitations()
logger.debug("CallController: updated call invitations chat")
}
await MainActor.run {
logger.debug("CallController.provider will answer on call")
if callManager.answerIncomingCall(callUUID: action.callUUID.uuidString.lowercased()) {
logger.debug("CallController.provider answered on call")
// WebRTC call should be in connected state to fulfill.
// Otherwise no audio and mic working on lockscreen
fulfillOnConnect = action
} else {
logger.debug("CallController.provider will fail the call")
action.fail()
}
}
if callManager.answerIncomingCall(callUUID: action.callUUID) {
// WebRTC call should be in connected state to fulfill.
// Otherwise no audio and mic working on lockscreen
fulfillOnConnect = action
} else {
action.fail()
}
}
@@ -93,7 +75,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
// Should be nil here if connection was in connected state
fulfillOnConnect?.fail()
fulfillOnConnect = nil
callManager.endCall(callUUID: action.callUUID.uuidString.lowercased()) { ok in
callManager.endCall(callUUID: action.callUUID) { ok in
if ok {
action.fulfill()
} else {
@@ -104,7 +86,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID.uuidString.lowercased()) {
if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID) {
action.fulfill()
} else {
action.fail()
@@ -174,19 +156,6 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
}
private func waitUntilChatStarted(timeoutMs: UInt64, stepMs: UInt64) async -> Bool {
logger.debug("CallController waiting until chat started")
var t: UInt64 = 0
repeat {
if ChatModel.shared.chatInitialized, ChatModel.shared.chatRunning == true, case .active = AppChatState.shared.value {
return true
}
_ = try? await Task.sleep(nanoseconds: stepMs * 1000000)
t += stepMs
} while t < timeoutMs
return false
}
@objc(pushRegistry:didUpdatePushCredentials:forType:)
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue)")
@@ -202,19 +171,32 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
self.reportExpiredCall(payload: payload, completion)
return
}
if (!ChatModel.shared.chatInitialized) {
logger.debug("CallController: initializing chat")
do {
try initializeChat(start: true, refreshInvitations: false)
} catch let error {
logger.error("CallController: initializing chat error: \(error)")
self.reportExpiredCall(payload: payload, completion)
return
}
}
logger.debug("CallController: initialized chat")
startChatForCall()
logger.debug("CallController: started chat")
self.shouldSuspendChat = true
// There are no invitations in the model, as it was processed by NSE
try? justRefreshCallInvitations()
logger.debug("CallController: updated call invitations chat")
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
// Extract the call information from the push notification payload
let m = ChatModel.shared
if let contactId = payload.dictionaryPayload["contactId"] as? String,
let displayName = payload.dictionaryPayload["displayName"] as? String,
let callUUID = payload.dictionaryPayload["callUUID"] as? String,
let uuid = UUID(uuidString: callUUID),
let callTsInterval = payload.dictionaryPayload["callTs"] as? TimeInterval,
let mediaStr = payload.dictionaryPayload["media"] as? String,
let media = CallMediaType(rawValue: mediaStr) {
let update = self.cxCallUpdate(contactId, displayName, media)
let callTs = Date(timeIntervalSince1970: callTsInterval)
if callTs.timeIntervalSinceNow >= -180 {
let invitation = m.callInvitations[contactId] {
let update = self.cxCallUpdate(invitation: invitation)
if let uuid = invitation.callkitUUID {
logger.debug("CallController: report pushkit call via CallKit")
let update = self.cxCallUpdate(invitation: invitation)
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
if error != nil {
m.callInvitations.removeValue(forKey: contactId)
@@ -223,31 +205,11 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
completion()
}
} else {
logger.debug("CallController will expire call 1")
self.reportExpiredCall(update: update, completion)
}
} else {
logger.debug("CallController will expire call 2")
self.reportExpiredCall(payload: payload, completion)
}
//DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
if (!ChatModel.shared.chatInitialized) {
logger.debug("CallController: initializing chat")
do {
try initializeChat(start: true, refreshInvitations: false)
} catch let error {
logger.error("CallController: initializing chat error: \(error)")
if let call = ChatModel.shared.activeCall {
self.endCall(call: call, completed: completion)
}
return
}
}
logger.debug("CallController: initialized chat")
startChatForCall()
logger.debug("CallController: started chat")
self.shouldSuspendChat = true
}
// This function fulfils the requirement to always report a call when PushKit notification is received,
@@ -277,8 +239,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callUUID))")
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID))")
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
if invitation.callTs.timeIntervalSinceNow >= -180 {
let update = cxCallUpdate(invitation: invitation)
provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
@@ -299,14 +261,6 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
return update
}
private func cxCallUpdate(_ contactId: String, _ displayName: String, _ media: CallMediaType) -> CXCallUpdate {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: contactId)
update.hasVideo = media == .video
update.localizedCallerName = displayName
return update
}
func reportIncomingCall(call: Call, connectedAt dateConnected: Date?) {
logger.debug("CallController: reporting incoming call connected")
if CallController.useCallKit() {
@@ -318,14 +272,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
logger.debug("CallController: reporting outgoing call connected")
if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) {
if CallController.useCallKit(), let uuid = call.callkitUUID {
provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected)
}
}
func reportCallRemoteEnded(invitation: RcvCallInvitation) {
logger.debug("CallController: reporting remote ended")
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
} else if invitation.contact.id == activeCallInvitation?.contact.id {
activeCallInvitation = nil
@@ -334,17 +288,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
func reportCallRemoteEnded(call: Call) {
logger.debug("CallController: reporting remote ended")
if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) {
if CallController.useCallKit(), let uuid = call.callkitUUID {
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
}
}
func startCall(_ contact: Contact, _ media: CallMediaType) {
logger.debug("CallController.startCall")
let callUUID = callManager.newOutgoingCall(contact, media)
guard let uuid = UUID(uuidString: callUUID) else {
return
}
let uuid = callManager.newOutgoingCall(contact, media)
if CallController.useCallKit() {
let handle = CXHandle(type: .generic, value: contact.id)
let action = CXStartCallAction(call: uuid, handle: handle)
@@ -356,8 +307,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
update.localizedCallerName = contact.displayName
self.provider.reportCall(with: uuid, updated: update)
}
} else if callManager.startOutgoingCall(callUUID: callUUID) {
if callManager.startOutgoingCall(callUUID: callUUID) {
} else if callManager.startOutgoingCall(callUUID: uuid) {
if callManager.startOutgoingCall(callUUID: uuid) {
logger.debug("CallController.startCall: call started")
} else {
logger.error("CallController.startCall: no active call")
@@ -367,8 +318,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
func answerCall(invitation: RcvCallInvitation) {
logger.debug("CallController: answering a call")
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
requestTransaction(with: CXAnswerCallAction(call: uuid))
if CallController.useCallKit(), let callUUID = invitation.callkitUUID {
requestTransaction(with: CXAnswerCallAction(call: callUUID))
} else {
callManager.answerIncomingCall(invitation: invitation)
}
@@ -377,13 +328,10 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
}
func endCall(callUUID: String) {
let uuid = UUID(uuidString: callUUID)
logger.debug("CallController: ending the call with UUID \(callUUID)")
func endCall(callUUID: UUID) {
logger.debug("CallController: ending the call with UUID \(callUUID.uuidString)")
if CallController.useCallKit() {
if let uuid {
requestTransaction(with: CXEndCallAction(call: uuid))
}
requestTransaction(with: CXEndCallAction(call: callUUID))
} else {
callManager.endCall(callUUID: callUUID) { ok in
if ok {
+13 -13
View File
@@ -10,17 +10,17 @@ import Foundation
import SimpleXChat
class CallManager {
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> String {
let uuid = UUID().uuidString.lowercased()
let call = Call(direction: .outgoing, contact: contact, callUUID: uuid, callState: .waitCapabilities, localMedia: media)
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID {
let uuid = UUID()
let call = Call(direction: .outgoing, contact: contact, callkitUUID: uuid, callState: .waitCapabilities, localMedia: media)
call.speakerEnabled = media == .video
ChatModel.shared.activeCall = call
return uuid
}
func startOutgoingCall(callUUID: String) -> Bool {
func startOutgoingCall(callUUID: UUID) -> Bool {
let m = ChatModel.shared
if let call = m.activeCall, call.callUUID == callUUID {
if let call = m.activeCall, call.callkitUUID == callUUID {
m.showCallView = true
Task { await m.callCommand.processCommand(.capabilities(media: call.localMedia)) }
return true
@@ -28,7 +28,7 @@ class CallManager {
return false
}
func answerIncomingCall(callUUID: String) -> Bool {
func answerIncomingCall(callUUID: UUID) -> Bool {
if let invitation = getCallInvitation(callUUID) {
answerIncomingCall(invitation: invitation)
return true
@@ -42,7 +42,7 @@ class CallManager {
let call = Call(
direction: .incoming,
contact: invitation.contact,
callUUID: invitation.callUUID,
callkitUUID: invitation.callkitUUID,
callState: .invitationAccepted,
localMedia: invitation.callType.media,
sharedKey: invitation.sharedKey
@@ -68,8 +68,8 @@ class CallManager {
}
}
func enableMedia(media: CallMediaType, enable: Bool, callUUID: String) -> Bool {
if let call = ChatModel.shared.activeCall, call.callUUID == callUUID {
func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool {
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
let m = ChatModel.shared
Task { await m.callCommand.processCommand(.media(media: media, enable: enable)) }
return true
@@ -77,8 +77,8 @@ class CallManager {
return false
}
func endCall(callUUID: String, completed: @escaping (Bool) -> Void) {
if let call = ChatModel.shared.activeCall, call.callUUID == callUUID {
func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) {
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
endCall(call: call) { completed(true) }
} else if let invitation = getCallInvitation(callUUID) {
endCall(invitation: invitation) { completed(true) }
@@ -126,8 +126,8 @@ class CallManager {
}
}
private func getCallInvitation(_ callUUID: String) -> RcvCallInvitation? {
if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callUUID == callUUID }) {
private func getCallInvitation(_ callUUID: UUID) -> RcvCallInvitation? {
if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callkitUUID == callUUID }) {
return invitation
}
return nil
+3 -3
View File
@@ -18,7 +18,7 @@ class Call: ObservableObject, Equatable {
var direction: CallDirection
var contact: Contact
var callUUID: String?
var callkitUUID: UUID?
var localMedia: CallMediaType
@Published var callState: CallState
@Published var localCapabilities: CallCapabilities?
@@ -33,14 +33,14 @@ class Call: ObservableObject, Equatable {
init(
direction: CallDirection,
contact: Contact,
callUUID: String?,
callkitUUID: UUID?,
callState: CallState,
localMedia: CallMediaType,
sharedKey: String? = nil
) {
self.direction = direction
self.contact = contact
self.callUUID = callUUID
self.callkitUUID = callkitUUID
self.callState = callState
self.localMedia = localMedia
self.sharedKey = sharedKey
@@ -942,7 +942,7 @@ func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Al
)
}
func queueInfoText(_ info: (RcvMsgInfo?, ServerQueueInfo)) -> String {
func queueInfoText(_ info: (RcvMsgInfo?, QueueInfo)) -> String {
let (rcvMsgInfo, qInfo) = info
var msgInfo: String
if let rcvMsgInfo { msgInfo = encodeJSON(rcvMsgInfo) } else { msgInfo = "none" }
@@ -12,6 +12,7 @@ import SimpleXChat
struct CIGroupInvitationView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Environment(\.showTimestamp) var showTimestamp
@ObservedObject var chat: Chat
var chatItem: ChatItem
var groupInvitation: CIGroupInvitation
@@ -45,7 +46,7 @@ struct CIGroupInvitationView: View {
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
.overlay(DetermineWidth())
}
@@ -53,7 +54,7 @@ struct CIGroupInvitationView: View {
(
groupInvitationText()
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
.overlay(DetermineWidth())
}
@@ -69,6 +70,7 @@ struct CIGroupInvitationView: View {
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.modifier(ChatTailPadding(chatItem: chatItem))
.background(chatItemFrameColor(chatItem, theme))
.textSelection(.disabled)
.onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 }
@@ -12,6 +12,7 @@ import SimpleXChat
struct CIMetaView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
@Environment(\.showTimestamp) var showTimestamp
var chatItem: ChatItem
var metaColor: Color
var paleMetaColor = Color(UIColor.tertiaryLabel)
@@ -30,24 +31,24 @@ struct CIMetaView: View {
switch meta.itemStatus {
case let .sndSent(sndProgress):
switch sndProgress {
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
case let .sndRcvd(_, sndProgress):
switch sndProgress {
case .complete:
ZStack {
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
case .partial:
ZStack {
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
}
default:
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
}
}
@@ -69,7 +70,8 @@ func ciMetaText(
sent: SentCheckmark? = nil,
showStatus: Bool = true,
showEdited: Bool = true,
showViaProxy: Bool
showViaProxy: Bool,
showTimesamp: Bool
) -> Text {
var r = Text("")
if showEdited, meta.itemEdited {
@@ -105,7 +107,9 @@ func ciMetaText(
if let enc = encrypted {
r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ")
}
r = r + meta.timestampText.foregroundColor(color)
if showTimesamp {
r = r + meta.timestampText.foregroundColor(color)
}
return r.font(.caption)
}
@@ -15,6 +15,7 @@ struct CIRcvDecryptionError: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
@Environment(\.showTimestamp) var showTimestamp
var msgDecryptError: MsgDecryptError
var msgCount: UInt32
var chatItem: ChatItem
@@ -68,37 +69,41 @@ struct CIRcvDecryptionError: View {
}
@ViewBuilder private func viewBody() -> some View {
if case let .direct(contact) = chat.chatInfo,
let contactStats = contact.activeConn?.connectionStats {
if contactStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncContactConnection(contact) }
Group {
if case let .direct(contact) = chat.chatInfo,
let contactStats = contact.activeConn?.connectionStats {
if contactStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncContactConnection(contact) }
}
} else if !contactStats.ratchetSyncSupported {
decryptionErrorItemFixButton(syncSupported: false) {
alert = .syncNotSupportedContactAlert
}
} else {
basicDecryptionErrorItem()
}
} else if !contactStats.ratchetSyncSupported {
decryptionErrorItemFixButton(syncSupported: false) {
alert = .syncNotSupportedContactAlert
} else if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir,
let mem = m.getGroupMember(groupMember.groupMemberId),
let memberStats = mem.wrapped.activeConn?.connectionStats {
if memberStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) }
}
} else if !memberStats.ratchetSyncSupported {
decryptionErrorItemFixButton(syncSupported: false) {
alert = .syncNotSupportedMemberAlert
}
} else {
basicDecryptionErrorItem()
}
} else {
basicDecryptionErrorItem()
}
} else if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember) = chatItem.chatDir,
let mem = m.getGroupMember(groupMember.groupMemberId),
let memberStats = mem.wrapped.activeConn?.connectionStats {
if memberStats.ratchetSyncAllowed {
decryptionErrorItemFixButton(syncSupported: true) {
alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) }
}
} else if !memberStats.ratchetSyncSupported {
decryptionErrorItemFixButton(syncSupported: false) {
alert = .syncNotSupportedMemberAlert
}
} else {
basicDecryptionErrorItem()
}
} else {
basicDecryptionErrorItem()
}
.modifier(ChatTailPadding(chatItem: chatItem))
.background(Color(uiColor: .tertiarySystemGroupedBackground))
}
private func basicDecryptionErrorItem() -> some View {
@@ -122,7 +127,7 @@ struct CIRcvDecryptionError: View {
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
.font(.callout)
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
)
}
.padding(.horizontal, 12)
@@ -131,7 +136,6 @@ struct CIRcvDecryptionError: View {
}
.onTapGesture(perform: { onClick() })
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.textSelection(.disabled)
}
@@ -142,7 +146,7 @@ struct CIRcvDecryptionError: View {
.foregroundColor(.red)
.italic()
+ Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
.padding(.horizontal, 12)
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
@@ -150,7 +154,6 @@ struct CIRcvDecryptionError: View {
}
.onTapGesture(perform: { onClick() })
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.textSelection(.disabled)
}
@@ -72,6 +72,7 @@ struct FramedItemView: View {
.accessibilityLabel("")
}
}
.modifier(ChatTailPadding(chatItem: chatItem))
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme))
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
@@ -69,6 +69,7 @@ struct CIMsgError: View {
}
.padding(.leading, 12)
.padding(.vertical, 6)
.modifier(ChatTailPadding(chatItem: chatItem))
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.textSelection(.disabled)
.onTapGesture(perform: onTap)
@@ -22,6 +22,7 @@ struct MarkedDeletedItemView: View {
.foregroundColor(theme.colors.secondary)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.modifier(ChatTailPadding(chatItem: chatItem))
.background(chatItemFrameColor(chatItem, theme))
.textSelection(.disabled)
}
@@ -26,6 +26,7 @@ private func typing(_ w: Font.Weight = .light) -> Text {
struct MsgContentView: View {
@ObservedObject var chat: Chat
@Environment(\.showTimestamp) var showTimestamp
@EnvironmentObject var theme: AppTheme
var text: String
var formattedText: [FormattedText]? = nil
@@ -84,7 +85,7 @@ struct MsgContentView: View {
}
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
}
}
@@ -9,9 +9,21 @@
import SwiftUI
import SimpleXChat
extension EnvironmentValues {
struct ShowTimestamp: EnvironmentKey {
static let defaultValue: Bool = true
}
var showTimestamp: Bool {
get { self[ShowTimestamp.self] }
set { self[ShowTimestamp.self] = newValue }
}
}
struct ChatItemView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
@Environment(\.showTimestamp) var showTimestamp
var chatItem: ChatItem
var maxWidth: CGFloat = .infinity
@Binding var revealed: Bool
+48 -23
View File
@@ -568,8 +568,8 @@ struct ChatView: View {
private func endCallButton(_ call: Call) -> some View {
Button {
if CallController.useCallKit(), let callUUID = call.callUUID {
CallController.shared.endCall(callUUID: callUUID)
if let uuid = call.callkitUUID {
CallController.shared.endCall(callUUID: uuid)
} else {
CallController.shared.endCall(call: call) {}
}
@@ -696,6 +696,7 @@ struct ChatView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding @ObservedObject var chat: Chat
@ObservedObject var dummyModel: ChatItemDummyModel = .shared
let chatItem: ChatItem
let maxWidth: CGFloat
@Binding var composeState: ComposeState
@@ -716,31 +717,53 @@ struct ChatView: View {
var revealed: Bool { chatItem == revealedChatItem }
struct TimeSeparation {
let isTimestampShown: Bool
let isGapLarge: Bool
init(for chatItem: ChatItem, at index: Int?) {
let im = ItemsModel.shared
if let index, index > 0 && !im.reversedChatItems.isEmpty {
let nextItem = im.reversedChatItems[index - 1]
isGapLarge = nextItem.chatDir != chatItem.chatDir ||
nextItem.meta.createdAt.timeIntervalSince(chatItem.meta.createdAt) > 30
isTimestampShown = isGapLarge ||
formatTimestampText(chatItem.meta.createdAt) != formatTimestampText(nextItem.meta.createdAt)
} else {
isTimestampShown = true
isGapLarge = true
}
}
}
var body: some View {
let (currIndex, _) = m.getNextChatItem(chatItem)
let ciCategory = chatItem.mergeCategory
let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory)
let range = itemsRange(currIndex, prevHidden)
let timeSeparation = TimeSeparation(for: chatItem, at: currIndex)
let im = ItemsModel.shared
Group {
if revealed, let range = range {
let items = Array(zip(Array(range), im.reversedChatItems[range]))
ForEach(items.reversed(), id: \.1.viewId) { (i, ci) in
let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
chatItemView(ci, nil, prev)
.overlay {
if let selected = selectedChatItems, ci.canBeDeletedForSelf {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
let checked = selected.contains(ci.id)
selectUnselectChatItem(select: !checked, ci)
VStack(spacing: 0) {
ForEach(items.reversed(), id: \.1.viewId) { (i: Int, ci: ChatItem) in
let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1]
chatItemView(ci, nil, prev, TimeSeparation(for: ci, at: i))
.overlay {
if let selected = selectedChatItems, ci.canBeDeletedForSelf {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
let checked = selected.contains(ci.id)
selectUnselectChatItem(select: !checked, ci)
}
}
}
}
}
}
} else {
chatItemView(chatItem, range, prevItem)
chatItemView(chatItem, range, prevItem, timeSeparation)
.overlay {
if let selected = selectedChatItems, chatItem.canBeDeletedForSelf {
Color.clear
@@ -791,7 +814,8 @@ struct ChatView: View {
}
}
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ prevItem: ChatItem?) -> some View {
@ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ prevItem: ChatItem?, _ timeSeparation: TimeSeparation) -> some View {
let bottomPadding: Double = timeSeparation.isGapLarge ? 6 : 2
if case let .groupRcv(member) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo {
let (prevMember, memCount): (GroupMember?, Int) =
@@ -833,11 +857,11 @@ struct ChatView: View {
}
}
}
chatItemWithMenu(ci, range, maxWidth)
chatItemWithMenu(ci, range, maxWidth, timeSeparation)
}
}
}
.padding(.bottom, 5)
.padding(.bottom, bottomPadding)
.padding(.trailing)
.padding(.leading, 12)
} else {
@@ -846,11 +870,11 @@ struct ChatView: View {
SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems)
.padding(.leading, 12)
}
chatItemWithMenu(ci, range, maxWidth)
chatItemWithMenu(ci, range, maxWidth, timeSeparation)
.padding(.trailing)
.padding(.leading, memberImageSize + 8 + 12)
}
.padding(.bottom, 5)
.padding(.bottom, bottomPadding)
}
} else {
HStack(alignment: .center, spacing: 0) {
@@ -863,10 +887,10 @@ struct ChatView: View {
.padding(.leading)
}
}
chatItemWithMenu(ci, range, maxWidth)
chatItemWithMenu(ci, range, maxWidth, timeSeparation)
.padding(.horizontal)
}
.padding(.bottom, 5)
.padding(.bottom, bottomPadding)
}
}
@@ -881,7 +905,7 @@ struct ChatView: View {
}
}
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat) -> some View {
@ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange<Int>?, _ maxWidth: CGFloat, _ timeSeparation: TimeSeparation) -> some View {
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
VStack(alignment: alignment.horizontal, spacing: 3) {
ChatItemView(
@@ -891,7 +915,8 @@ struct ChatView: View {
revealed: .constant(revealed),
allowMenu: $allowMenu
)
.modifier(ChatItemClipped(ci))
.environment(\.showTimestamp, timeSeparation.isTimestampShown)
.modifier(ChatItemClipped(ci, isTailVisible: timeSeparation.isGapLarge))
.contextMenu { menu(ci, range, live: composeState.liveMessage != nil) }
.accessibilityLabel("")
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
@@ -751,6 +751,7 @@ struct ComposeView: View {
case .linkPreview:
sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl)
case let .mediaPreviews(mediaPreviews: media):
// TODO batch send: batch media previews
let last = media.count - 1
if last >= 0 {
for i in 0..<last {
@@ -887,22 +888,26 @@ struct ComposeView: View {
}
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
if let chatItem = chat.chatInfo.chatType == .local
? await apiCreateChatItem(noteFolderId: chat.chatInfo.apiId, file: file, msg: mc)
: await apiSendMessage(
if let chatItems = chat.chatInfo.chatType == .local
? await apiCreateChatItems(
noteFolderId: chat.chatInfo.apiId,
composedMessages: [ComposedMessage(fileSource: file, msgContent: mc)]
)
: await apiSendMessages(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
file: file,
quotedItemId: quoted,
msg: mc,
live: live,
ttl: ttl
ttl: ttl,
composedMessages: [ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc)]
) {
await MainActor.run {
chatModel.removeLiveDummy(animated: false)
chatModel.addChatItem(chat.chatInfo, chatItem)
for chatItem in chatItems {
chatModel.addChatItem(chat.chatInfo, chatItem)
}
}
return chatItem
// UI only supports sending one item at a time
return chatItems.first
}
if let file = file {
removeFile(file.filePath)
@@ -911,18 +916,21 @@ struct ComposeView: View {
}
func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> ChatItem? {
if let chatItem = await apiForwardChatItem(
if let chatItems = await apiForwardChatItems(
toChatType: chat.chatInfo.chatType,
toChatId: chat.chatInfo.apiId,
fromChatType: fromChatInfo.chatType,
fromChatId: fromChatInfo.apiId,
itemId: forwardedItem.id,
itemIds: [forwardedItem.id],
ttl: ttl
) {
await MainActor.run {
chatModel.addChatItem(chat.chatInfo, chatItem)
for chatItem in chatItems {
chatModel.addChatItem(chat.chatInfo, chatItem)
}
}
return chatItem
// TODO batch send: forward multiple messages
return chatItems.first
}
return nil
}
@@ -14,55 +14,150 @@ import SimpleXChat
/// Supports [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically)
/// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius
struct ChatItemClipped: ViewModifier {
struct ClipShape: Shape {
let maxCornerRadius: Double
func path(in rect: CGRect) -> Path {
Path(
roundedRect: rect,
cornerRadius: min((rect.height / 2), maxCornerRadius),
style: .circular
)
@AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var isTailEnabled = true
let chatItem: ChatItem?
let isTailVisible: Bool
init() {
chatItem = nil
isTailVisible = false
}
init(_ chatItem: ChatItem, isTailVisible: Bool) {
self.chatItem = chatItem
self.isTailVisible = isTailVisible
}
fileprivate func itemShape() -> ChatItemShape {
if let chatItem {
switch chatItem.content {
case
.sndMsgContent,
.rcvMsgContent,
.rcvDecryptionError,
.rcvGroupInvitation,
.sndGroupInvitation,
.sndDeleted,
.rcvDeleted,
.rcvIntegrityError,
.sndModerated,
.rcvModerated,
.rcvBlocked,
.invalidJSON: isTailEnabled
? .bubble(
padding: chatItem.chatDir.sent ? .trailing : .leading,
isTailVisible: Self.hidesTail(chatItem.content.msgContent)
? false
: isTailVisible
)
: .roundRect(maxRadius: ChatBubble.maxRadius)
default: .roundRect(maxRadius: 8)
}
} else {
.roundRect(maxRadius: 8)
}
}
init() {
clipShape = ClipShape(
maxCornerRadius: 18
)
// Tail is hidden for images and video without any text
private static func hidesTail(_ msgContent: MsgContent?) -> Bool {
if let msgContent, msgContent.isImageOrVideo && msgContent.text.isEmpty {
true
} else {
false
}
}
init(_ chatItem: ChatItem) {
clipShape = ClipShape(
maxCornerRadius: {
switch chatItem.content {
case
.sndMsgContent,
.rcvMsgContent,
.rcvDecryptionError,
.rcvGroupInvitation,
.sndGroupInvitation,
.sndDeleted,
.rcvDeleted,
.rcvIntegrityError,
.sndModerated,
.rcvModerated,
.rcvBlocked,
.invalidJSON: 18
default: 8
}
}()
)
}
private let clipShape: ClipShape
func body(content: Content) -> some View {
let shape = ChatBubble(roundness: roundness, shapePath: itemShape())
content
.contentShape(.dragPreview, clipShape)
.contentShape(.contextMenuPreview, clipShape)
.clipShape(clipShape)
.contentShape(.dragPreview, shape)
.contentShape(.contextMenuPreview, shape)
.clipShape(shape)
}
}
struct ChatTailPadding: ViewModifier {
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var tailEnabled = true
let chatItem: ChatItem
func body(content: Content) -> some View {
if tailEnabled {
content.padding(
chatItem.chatDir.sent ? .trailing : .leading,
ChatBubble.tailSize
)
} else {
content
}
}
}
struct ChatBubble: Shape {
static let tailSize: Double = 8
static let maxRadius: Double = 16
fileprivate let roundness: Double
fileprivate let shapePath: ChatItemShape
func path(in rect: CGRect) -> Path {
switch shapePath {
case .bubble(let padding, let isTailVisible):
let rMax = min(Self.maxRadius, min(rect.width, rect.height) / 2)
let r = roundness * rMax
let tailHeight = rect.height - (Self.tailSize + (rMax - Self.tailSize) * roundness)
var path = Path()
path.addArc(
center: CGPoint(x: r + Self.tailSize, y: r),
radius: r,
startAngle: .degrees(270),
endAngle: .degrees(180),
clockwise: true
)
if isTailVisible {
path.addLine(
to: CGPoint(x: Self.tailSize, y: tailHeight)
)
path.addQuadCurve(
to: CGPoint(x: 0, y: rect.height),
control: CGPoint(x: Self.tailSize, y: tailHeight + r * 0.64)
)
} else {
path.addArc(
center: CGPoint(x: r + Self.tailSize, y: rect.height - r),
radius: r,
startAngle: .degrees(180),
endAngle: .degrees(90),
clockwise: true
)
}
path.addArc(
center: CGPoint(x: rect.width - r, y: rect.height - r),
radius: r,
startAngle: .degrees(90),
endAngle: .degrees(0),
clockwise: true
)
path.addArc(
center: CGPoint(x: rect.width - r, y: r),
radius: r,
startAngle: .degrees(0),
endAngle: .degrees(270),
clockwise: true
)
return switch padding {
case .leading: path
case .trailing: path
.scale(x: -1, y: 1, anchor: .center)
.path(in: rect)
}
case let .roundRect(radius):
return Path(roundedRect: rect, cornerRadius: radius * roundness)
}
}
}
fileprivate enum ChatItemShape {
case bubble(padding: HorizontalEdge, isTailVisible: Bool)
case roundRect(maxRadius: Double)
}
@@ -28,7 +28,9 @@ struct AddContactLearnMore: View {
Text("If you can't meet in person, show QR code in a video call, or share the link.")
Text("Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).")
}
.frame(maxWidth: .infinity, alignment: .leading)
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.modifier(ThemedBackground(grouped: true))
}
@@ -14,9 +14,10 @@ enum ContactType: Int {
}
struct NewChatMenuButton: View {
@EnvironmentObject var chatModel: ChatModel
@State private var showNewChatSheet = false
@State private var alert: SomeAlert? = nil
@State private var globalAlert: SomeAlert? = nil
@State private var pendingConnection: PendingContactConnection? = nil
var body: some View {
Button {
@@ -28,22 +29,14 @@ struct NewChatMenuButton: View {
.frame(width: 24, height: 24)
}
.appSheet(isPresented: $showNewChatSheet) {
NewChatSheet(alert: $alert)
NewChatSheet(pendingConnection: $pendingConnection)
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
.alert(item: $alert) { a in
return a.alert
.onDisappear {
alert = cleanupPendingConnection(chatModel: chatModel, contactConnection: pendingConnection)
pendingConnection = nil
}
}
// This is a workaround to show "Keep unused invitation" alert in both following cases:
// - on going back from NewChatView to NewChatSheet,
// - on dismissing NewChatMenuButton sheet while on NewChatView (skipping NewChatSheet)
.onChange(of: alert?.id) { a in
if !showNewChatSheet && alert != nil {
globalAlert = alert
alert = nil
}
}
.alert(item: $globalAlert) { a in
.alert(item: $alert) { a in
return a.alert
}
}
@@ -60,7 +53,8 @@ struct NewChatSheet: View {
@State private var searchText = ""
@State private var searchShowingSimplexLink = false
@State private var searchChatFilteredBySimplexLink: String? = nil
@Binding var alert: SomeAlert?
@State private var alert: SomeAlert?
@Binding var pendingConnection: PendingContactConnection?
// Sheet height management
@State private var isAddContactActive = false
@@ -78,6 +72,9 @@ struct NewChatSheet: View {
.navigationBarTitleDisplayMode(.large)
.navigationBarHidden(searchMode)
.modifier(ThemedBackground(grouped: true))
.alert(item: $alert) { a in
return a.alert
}
}
if #available(iOS 16.0, *), oneHandUI {
let sheetHeight: CGFloat = showArchive ? 575 : 500
@@ -112,7 +109,7 @@ struct NewChatSheet: View {
if (searchText.isEmpty) {
Section {
NavigationLink(isActive: $isAddContactActive) {
NewChatView(selection: .invite, parentAlert: $alert)
NewChatView(selection: .invite, parentAlert: $alert, contactConnection: $pendingConnection)
.navigationTitle("New chat")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
@@ -122,7 +119,7 @@ struct NewChatSheet: View {
}
}
NavigationLink(isActive: $isScanPasteLinkActive) {
NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert)
NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert, contactConnection: $pendingConnection)
.navigationTitle("New chat")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
+325 -42
View File
@@ -45,18 +45,47 @@ enum NewChatOption: Identifiable {
var id: Self { self }
}
func cleanupPendingConnection(chatModel: ChatModel, contactConnection: PendingContactConnection?) -> SomeAlert? {
var alert: SomeAlert? = nil
if !(chatModel.showingInvitation?.connChatUsed ?? true),
let conn = contactConnection {
alert = SomeAlert(
alert: Alert(
title: Text("Keep unused invitation?"),
message: Text("You can view invitation link again in connection details."),
primaryButton: .default(Text("Keep")) {},
secondaryButton: .destructive(Text("Delete")) {
Task {
await deleteChat(Chat(
chatInfo: .contactConnection(contactConnection: conn),
chatItems: []
))
}
}
),
id: "keepUnusedInvitation"
)
}
chatModel.showingInvitation = nil
return alert
}
struct NewChatView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@State var selection: NewChatOption
@State var showQRCodeScanner = false
@State private var invitationUsed: Bool = false
@State private var contactConnection: PendingContactConnection? = nil
@State private var connReqInvitation: String = ""
@State private var creatingConnReq = false
@State var choosingProfile = false
@State private var pastedLink: String = ""
@State private var alert: NewChatViewAlert?
@Binding var parentAlert: SomeAlert?
@Binding var contactConnection: PendingContactConnection?
var body: some View {
VStack(alignment: .leading) {
@@ -122,26 +151,10 @@ struct NewChatView: View {
}
}
.onDisappear {
if !(m.showingInvitation?.connChatUsed ?? true),
let conn = contactConnection {
parentAlert = SomeAlert(
alert: Alert(
title: Text("Keep unused invitation?"),
message: Text("You can view invitation link again in connection details."),
primaryButton: .default(Text("Keep")) {},
secondaryButton: .destructive(Text("Delete")) {
Task {
await deleteChat(Chat(
chatInfo: .contactConnection(contactConnection: conn),
chatItems: []
))
}
}
),
id: "keepUnusedInvitation"
)
if !choosingProfile {
parentAlert = cleanupPendingConnection(chatModel: m, contactConnection: contactConnection)
contactConnection = nil
}
m.showingInvitation = nil
}
.alert(item: $alert) { a in
switch(a) {
@@ -159,7 +172,8 @@ struct NewChatView: View {
InviteView(
invitationUsed: $invitationUsed,
contactConnection: $contactConnection,
connReqInvitation: connReqInvitation
connReqInvitation: $connReqInvitation,
choosingProfile: $choosingProfile
)
} else if creatingConnReq {
creatingLinkProgressView()
@@ -210,13 +224,24 @@ struct NewChatView: View {
}
}
private func incognitoProfileImage() -> some View {
Image(systemName: "theatermasks.fill")
.resizable()
.scaledToFit()
.frame(width: 30)
.foregroundColor(.indigo)
}
private struct InviteView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var invitationUsed: Bool
@Binding var contactConnection: PendingContactConnection?
var connReqInvitation: String
@Binding var connReqInvitation: String
@Binding var choosingProfile: Bool
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@State private var showSettings: Bool = false
var body: some View {
List {
@@ -226,28 +251,40 @@ private struct InviteView: View {
.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10))
qrCodeView()
Section {
IncognitoToggle(incognitoEnabled: $incognitoDefault)
} footer: {
sharedProfileInfo(incognitoDefault)
.foregroundColor(theme.colors.secondary)
if let selectedProfile = chatModel.currentUser {
Section {
NavigationLink {
ActiveProfilePicker(
contactConnection: $contactConnection,
connReqInvitation: $connReqInvitation,
incognitoEnabled: $incognitoDefault,
choosingProfile: $choosingProfile,
selectedProfile: selectedProfile
)
} label: {
HStack {
if incognitoDefault {
incognitoProfileImage()
Text("Incognito")
} else {
ProfileImage(imageStr: chatModel.currentUser?.image, size: 30)
Text(chatModel.currentUser?.chatViewName ?? "")
}
}
}
} header: {
Text("Share profile").foregroundColor(theme.colors.secondary)
} footer: {
if incognitoDefault {
Text("A new random profile will be shared.")
}
}
}
}
.onChange(of: incognitoDefault) { incognito in
Task {
do {
if let contactConn = contactConnection,
let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
await MainActor.run {
contactConnection = conn
chatModel.updateContactConnection(conn)
}
}
} catch {
logger.error("apiSetConnectionIncognito error: \(responseError(error))")
}
}
setInvitationUsed()
}
.onChange(of: chatModel.currentUser) { u in
setInvitationUsed()
}
}
@@ -270,6 +307,7 @@ private struct InviteView: View {
private func qrCodeView() -> some View {
Section(header: Text("Or show this code").foregroundColor(theme.colors.secondary)) {
SimpleXLinkQRCode(uri: connReqInvitation, onShare: setInvitationUsed)
.id("simplex-qrcode-view-for-\(connReqInvitation)")
.padding()
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
@@ -289,6 +327,249 @@ private struct InviteView: View {
}
}
private struct ActiveProfilePicker: View {
@Environment(\.dismiss) var dismiss
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@Binding var contactConnection: PendingContactConnection?
@Binding var connReqInvitation: String
@Binding var incognitoEnabled: Bool
@Binding var choosingProfile: Bool
@State private var alert: SomeAlert?
@State private var switchingProfile = false
@State private var switchingProfileByTimeout = false
@State private var lastSwitchingProfileByTimeoutCall: Double?
@State private var profiles: [User] = []
@State private var searchTextOrPassword = ""
@State private var showIncognitoSheet = false
@State private var incognitoFirst: Bool = false
@State var selectedProfile: User
var trimmedSearchTextOrPassword: String { searchTextOrPassword.trimmingCharacters(in: .whitespaces)}
var body: some View {
viewBody()
.navigationTitle("Select chat profile")
.searchable(text: $searchTextOrPassword, placement: .navigationBarDrawer(displayMode: .always))
.autocorrectionDisabled(true)
.navigationBarTitleDisplayMode(.large)
.onAppear {
profiles = chatModel.users
.map { $0.user }
.sorted { u, _ in u.activeUser }
}
.onChange(of: incognitoEnabled) { incognito in
if !switchingProfile {
return
}
Task {
do {
if let contactConn = contactConnection,
let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) {
await MainActor.run {
contactConnection = conn
chatModel.updateContactConnection(conn)
switchingProfile = false
dismiss()
}
}
} catch {
switchingProfile = false
incognitoEnabled = !incognito
logger.error("apiSetConnectionIncognito error: \(responseError(error))")
let err = getErrorAlert(error, "Error changing to incognito!")
alert = SomeAlert(
alert: Alert(
title: Text(err.title),
message: Text(err.message ?? "Error: \(responseError(error))")
),
id: "setConnectionIncognitoError"
)
}
}
}
.onChange(of: switchingProfile) { sp in
if sp {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
switchingProfileByTimeout = switchingProfile
}
} else {
switchingProfileByTimeout = false
}
}
.onChange(of: selectedProfile) { profile in
if (profile == chatModel.currentUser) {
return
}
Task {
do {
switchingProfile = true
if let contactConn = contactConnection,
let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) {
await MainActor.run {
contactConnection = conn
connReqInvitation = conn.connReqInv ?? ""
chatModel.updateContactConnection(conn)
}
do {
try await changeActiveUserAsync_(profile.userId, viewPwd: profile.hidden ? trimmedSearchTextOrPassword : nil )
await MainActor.run {
switchingProfile = false
dismiss()
}
} catch {
await MainActor.run {
switchingProfile = false
alert = SomeAlert(
alert: Alert(
title: Text("Error switching profile"),
message: Text("Your connection was moved to \(profile.chatViewName) but an unexpected error occurred while redirecting you to the profile.")
),
id: "switchingProfileError"
)
}
}
}
} catch {
await MainActor.run {
switchingProfile = false
if let currentUser = chatModel.currentUser {
selectedProfile = currentUser
}
let err = getErrorAlert(error, "Error changing connection profile")
alert = SomeAlert(
alert: Alert(
title: Text(err.title),
message: Text(err.message ?? "Error: \(responseError(error))")
),
id: "changeConnectionUserError"
)
}
}
}
}
.alert(item: $alert) { a in
a.alert
}
.onAppear {
incognitoFirst = incognitoEnabled
choosingProfile = true
}
.onDisappear {
choosingProfile = false
}
.sheet(isPresented: $showIncognitoSheet) {
IncognitoHelp()
}
}
@ViewBuilder private func viewBody() -> some View {
profilePicker()
.allowsHitTesting(!switchingProfileByTimeout)
.modifier(ThemedBackground(grouped: true))
.overlay {
if switchingProfileByTimeout {
ProgressView()
.scaleEffect(2)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
private func filteredProfiles() -> [User] {
let s = trimmedSearchTextOrPassword
let lower = s.localizedLowercase
return profiles.filter { u in
if (u.activeUser || !u.hidden) && (s == "" || u.chatViewName.localizedLowercase.contains(lower)) {
return true
}
return correctPassword(u, s)
}
}
@ViewBuilder private func profilerPickerUserOption(_ user: User) -> some View {
Button {
if selectedProfile != user || incognitoEnabled {
switchingProfile = true
incognitoEnabled = false
selectedProfile = user
}
} label: {
HStack {
ProfileImage(imageStr: user.image, size: 30)
.padding(.trailing, 2)
Text(user.chatViewName)
.foregroundColor(theme.colors.onBackground)
.lineLimit(1)
Spacer()
if selectedProfile == user, !incognitoEnabled {
Image(systemName: "checkmark")
.resizable().scaledToFit().frame(width: 16)
.foregroundColor(theme.colors.primary)
}
}
}
}
@ViewBuilder private func profilePicker() -> some View {
let incognitoOption = Button {
if !incognitoEnabled {
incognitoEnabled = true
switchingProfile = true
}
} label : {
HStack {
incognitoProfileImage()
Text("Incognito")
.foregroundColor(theme.colors.onBackground)
Image(systemName: "info.circle")
.foregroundColor(theme.colors.primary)
.font(.system(size: 14))
.onTapGesture {
showIncognitoSheet = true
}
Spacer()
if incognitoEnabled {
Image(systemName: "checkmark")
.resizable().scaledToFit().frame(width: 16)
.foregroundColor(theme.colors.primary)
}
}
}
List {
let filteredProfiles = filteredProfiles()
let activeProfile = filteredProfiles.first { u in u.activeUser }
if let selectedProfile = activeProfile {
let otherProfiles = filteredProfiles.filter { u in u.userId != activeProfile?.userId }
if incognitoFirst {
incognitoOption
profilerPickerUserOption(selectedProfile)
} else {
profilerPickerUserOption(selectedProfile)
incognitoOption
}
ForEach(otherProfiles) { p in
profilerPickerUserOption(p)
}
} else {
incognitoOption
ForEach(filteredProfiles) { p in
profilerPickerUserOption(p)
}
}
}
.opacity(switchingProfileByTimeout ? 0.4 : 1)
}
}
private struct ConnectView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var theme: AppTheme
@@ -975,10 +1256,12 @@ func connReqSentAlert(_ type: ConnReqType) -> Alert {
struct NewChatView_Previews: PreviewProvider {
static var previews: some View {
@State var parentAlert: SomeAlert?
@State var contactConnection: PendingContactConnection? = nil
NewChatView(
selection: .invite,
parentAlert: $parentAlert
parentAlert: $parentAlert,
contactConnection: $contactConnection
)
}
}
+1 -1
View File
@@ -160,7 +160,7 @@ struct TerminalView_Previews: PreviewProvider {
let chatModel = ChatModel()
chatModel.terminalItems = [
.resp(.now, ChatResponse.response(type: "contactSubscribed", json: "{}")),
.resp(.now, ChatResponse.response(type: "newChatItem", json: "{}"))
.resp(.now, ChatResponse.response(type: "newChatItems", json: "{}"))
]
return NavigationView {
TerminalView()
@@ -33,6 +33,8 @@ struct AppearanceSettings: View {
}()
@State private var darkModeTheme: String = UserDefaults.standard.string(forKey: DEFAULT_SYSTEM_DARK_THEME) ?? DefaultTheme.DARK.themeName
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner
@AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var chatItemRoundness = defaultChatItemRoundness
@AppStorage(DEFAULT_CHAT_ITEM_TAIL) private var chatItemTail = true
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
@@ -179,6 +181,14 @@ struct AppearanceSettings: View {
}
}
Section(header: Text("Messages").foregroundColor(theme.colors.secondary)) {
HStack {
Text("Roundness")
Slider(value: $chatItemRoundness, in: 0...1, step: 0.1)
}
Toggle("Tail", isOn: $chatItemTail)
}
Section(header: Text("Profile images").foregroundColor(theme.colors.secondary)) {
HStack(spacing: 16) {
if let img = m.currentUser?.image, img != "" {
@@ -358,13 +368,13 @@ struct ChatThemePreview: View {
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
HStack {
ChatItemView(chat: Chat.sampleData, chatItem: alice, revealed: Binding.constant(false))
.modifier(ChatItemClipped())
.modifier(ChatItemClipped(alice, isTailVisible: true))
Spacer()
}
HStack {
Spacer()
ChatItemView(chat: Chat.sampleData, chatItem: bob, revealed: Binding.constant(false))
.modifier(ChatItemClipped())
.modifier(ChatItemClipped(bob, isTailVisible: true))
.frame(alignment: .trailing)
}
} else {
@@ -26,6 +26,7 @@ struct IncognitoHelp: View {
Text("Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).")
}
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.modifier(ThemedBackground())
}
@@ -47,6 +47,8 @@ let DEFAULT_ACCENT_COLOR_GREEN = "accentColorGreen" // deprecated, only used for
let DEFAULT_ACCENT_COLOR_BLUE = "accentColorBlue" // deprecated, only used for migration
let DEFAULT_USER_INTERFACE_STYLE = "userInterfaceStyle" // deprecated, only used for migration
let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
let DEFAULT_CHAT_ITEM_ROUNDNESS = "chatItemRoundness"
let DEFAULT_CHAT_ITEM_TAIL = "chatItemTail"
let DEFAULT_ONE_HAND_UI_CARD_SHOWN = "oneHandUICardShown"
let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial"
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
@@ -98,6 +100,8 @@ let appDefaults: [String: Any] = [
DEFAULT_DEVELOPER_TOOLS: false,
DEFAULT_ENCRYPTION_STARTED: false,
DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner,
DEFAULT_CHAT_ITEM_ROUNDNESS: defaultChatItemRoundness,
DEFAULT_CHAT_ITEM_TAIL: true,
DEFAULT_ONE_HAND_UI_CARD_SHOWN: false,
DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial,
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
@@ -406,6 +406,13 @@ public func chatPasswordHash(_ pwd: String, _ salt: String) -> String {
return hash
}
public func correctPassword(_ user: User, _ pwd: String) -> Bool {
if let ph = user.viewPwdHash {
return pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash
}
return false
}
struct UserProfilesView_Previews: PreviewProvider {
static var previews: some View {
UserProfilesView(showSettings: Binding.constant(true))
+16 -13
View File
@@ -339,9 +339,7 @@ class NotificationService: UNNotificationServiceExtension {
CXProvider.reportNewIncomingVoIPPushPayload([
"displayName": invitation.contact.displayName,
"contactId": invitation.contact.id,
"callUUID": invitation.callUUID ?? "",
"media": invitation.callType.media.rawValue,
"callTs": invitation.callTs.timeIntervalSince1970
"media": invitation.callType.media.rawValue
]) { error in
logger.debug("reportNewIncomingVoIPPushPayload result: \(error)")
deliver(error == nil ? nil : createCallInvitationNtf(invitation))
@@ -573,17 +571,22 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
// TODO profile update
case let .receivedContactRequest(user, contactRequest):
return (UserContact(contactRequest: contactRequest).id, .nse(createContactRequestNtf(user, contactRequest)))
case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo
var cItem = aChatItem.chatItem
if !cInfo.ntfsEnabled {
ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1))
case let .newChatItems(user, chatItems):
// Received items are created one at a time
if let chatItem = chatItems.first {
let cInfo = chatItem.chatInfo
var cItem = chatItem.chatItem
if !cInfo.ntfsEnabled {
ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1))
}
if let file = cItem.autoReceiveFile() {
cItem = autoReceiveFile(file) ?? cItem
}
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(createMessageReceivedNtf(user, cInfo, cItem)) : .empty
return cItem.showNotification ? (chatItem.chatId, ntf) : nil
} else {
return nil
}
if let file = cItem.autoReceiveFile() {
cItem = autoReceiveFile(file) ?? cItem
}
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(createMessageReceivedNtf(user, cInfo, cItem)) : .empty
return cItem.showNotification ? (aChatItem.chatId, ntf) : nil
case let .rcvFileSndCancelled(_, aChatItem, _):
cleanupFile(aChatItem)
return nil
+13 -15
View File
@@ -54,32 +54,30 @@ func apiGetChats(userId: User.ID) throws -> Array<ChatData> {
throw r
}
func apiSendMessage(
func apiSendMessages(
chatInfo: ChatInfo,
cryptoFile: CryptoFile?,
msgContent: MsgContent
) throws -> AChatItem {
composedMessages: [ComposedMessage]
) throws -> [AChatItem] {
let r = sendSimpleXCmd(
chatInfo.chatType == .local
? .apiCreateChatItem(
? .apiCreateChatItems(
noteFolderId: chatInfo.apiId,
file: cryptoFile,
msg: msgContent
composedMessages: composedMessages
)
: .apiSendMessage(
: .apiSendMessages(
type: chatInfo.chatType,
id: chatInfo.apiId,
file: cryptoFile,
quotedItemId: nil,
msg: msgContent,
live: false,
ttl: nil
ttl: nil,
composedMessages: composedMessages
)
)
if case let .newChatItem(_, chatItem) = r {
return chatItem
if case let .newChatItems(_, chatItems) = r {
return chatItems
} else {
if let filePath = cryptoFile?.filePath { removeFile(filePath) }
for composedMessage in composedMessages {
if let filePath = composedMessage.fileSource?.filePath { removeFile(filePath) }
}
throw r
}
}
+14 -12
View File
@@ -141,23 +141,25 @@ class ShareModel: ObservableObject {
do {
SEChatState.shared.set(.sendingMessage)
await waitForOtherProcessesToSuspend()
let ci = try apiSendMessage(
let chatItems = try apiSendMessages(
chatInfo: selected.chatInfo,
cryptoFile: sharedContent.cryptoFile,
msgContent: sharedContent.msgContent(comment: self.comment)
composedMessages: [ComposedMessage(fileSource: sharedContent.cryptoFile, msgContent: sharedContent.msgContent(comment: self.comment))]
)
if selected.chatInfo.chatType == .local {
completion()
} else {
await MainActor.run { self.bottomBar = .loadingBar(progress: 0) }
if let e = await handleEvents(
isGroupChat: ci.chatInfo.chatType == .group,
isWithoutFile: sharedContent.cryptoFile == nil,
chatItemId: ci.chatItem.id
) {
await MainActor.run { errorAlert = e }
} else {
completion()
// TODO batch send: share multiple items
if let ci = chatItems.first {
await MainActor.run { self.bottomBar = .loadingBar(progress: 0) }
if let e = await handleEvents(
isGroupChat: ci.chatInfo.chatType == .group,
isWithoutFile: sharedContent.cryptoFile == nil,
chatItemId: ci.chatItem.id
) {
await MainActor.run { errorAlert = e }
} else {
completion()
}
}
}
} catch {
+40 -40
View File
@@ -214,11 +214,11 @@
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; };
E5BD844D2C8220D0008C24D1 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84482C8220D0008C24D1 /* libffi.a */; };
E5BD844E2C8220D0008C24D1 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84492C8220D0008C24D1 /* libgmpxx.a */; };
E5BD844F2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD844A2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a */; };
E5BD84502C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD844B2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a */; };
E5BD84512C8220D0008C24D1 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD844C2C8220D0008C24D1 /* libgmp.a */; };
E51ED5802C78BF95009F2C7C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57B2C78BF95009F2C7C /* libgmpxx.a */; };
E51ED5812C78BF95009F2C7C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57C2C78BF95009F2C7C /* libgmp.a */; };
E51ED5822C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57D2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a */; };
E51ED5832C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57E2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a */; };
E51ED5842C78BF95009F2C7C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED57F2C78BF95009F2C7C /* libffi.a */; };
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; };
@@ -550,11 +550,11 @@
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; };
E5BD84482C8220D0008C24D1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E5BD84492C8220D0008C24D1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
E5BD844A2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a"; sourceTree = "<group>"; };
E5BD844B2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a"; sourceTree = "<group>"; };
E5BD844C2C8220D0008C24D1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E51ED57B2C78BF95009F2C7C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
E51ED57C2C78BF95009F2C7C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E51ED57D2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a"; sourceTree = "<group>"; };
E51ED57E2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a"; sourceTree = "<group>"; };
E51ED57F2C78BF95009F2C7C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -645,14 +645,14 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E51ED5802C78BF95009F2C7C /* libgmpxx.a in Frameworks */,
E51ED5812C78BF95009F2C7C /* libgmp.a in Frameworks */,
E51ED5822C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a in Frameworks */,
E51ED5842C78BF95009F2C7C /* libffi.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
E5BD844E2C8220D0008C24D1 /* libgmpxx.a in Frameworks */,
E5BD84512C8220D0008C24D1 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
E5BD84502C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a in Frameworks */,
E5BD844D2C8220D0008C24D1 /* libffi.a in Frameworks */,
E5BD844F2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a in Frameworks */,
E51ED5832C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -729,11 +729,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
E5BD84482C8220D0008C24D1 /* libffi.a */,
E5BD844C2C8220D0008C24D1 /* libgmp.a */,
E5BD84492C8220D0008C24D1 /* libgmpxx.a */,
E5BD844A2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a */,
E5BD844B2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a */,
E51ED57F2C78BF95009F2C7C /* libffi.a */,
E51ED57C2C78BF95009F2C7C /* libgmp.a */,
E51ED57B2C78BF95009F2C7C /* libgmpxx.a */,
E51ED57E2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA-ghc9.6.3.a */,
E51ED57D2C78BF95009F2C7C /* libHSsimplex-chat-6.1.0.0-29ba1DCA1ro2ZCUgGLJUlA.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -1879,7 +1879,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 234;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1904,7 +1904,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1928,7 +1928,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 234;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1953,7 +1953,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1969,11 +1969,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 234;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -1989,11 +1989,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 234;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2014,7 +2014,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 234;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2029,7 +2029,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2051,7 +2051,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 234;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2066,7 +2066,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2088,7 +2088,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 234;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2114,7 +2114,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2139,7 +2139,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 234;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2165,7 +2165,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.2;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2190,7 +2190,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 234;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2205,7 +2205,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2224,7 +2224,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 236;
CURRENT_PROJECT_VERSION = 234;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2239,7 +2239,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.0.4;
MARKETING_VERSION = 6.0.2;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
+73 -68
View File
@@ -42,13 +42,13 @@ public enum ChatCommand {
case apiGetChats(userId: Int64)
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String)
case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64)
case apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?)
case apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent)
case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage])
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool)
case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode)
case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64])
case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction)
case apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?)
case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?)
case apiGetNtfToken
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
@@ -97,6 +97,7 @@ public enum ChatCommand {
case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?)
case apiAddContact(userId: Int64, incognito: Bool)
case apiSetConnectionIncognito(connId: Int64, incognito: Bool)
case apiChangeConnectionUser(connId: Int64, userId: Int64)
case apiConnectPlan(userId: Int64, connReq: String)
case apiConnect(userId: Int64, incognito: Bool, connReq: String)
case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64)
@@ -190,20 +191,20 @@ public enum ChatCommand {
case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" +
(search == "" ? "" : " search=\(search)")
case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)"
case let .apiSendMessage(type, id, file, quotedItemId, mc, live, ttl):
let msg = encodeJSON(ComposedMessage(fileSource: file, quotedItemId: quotedItemId, msgContent: mc))
case let .apiSendMessages(type, id, live, ttl, composedMessages):
let msgs = encodeJSON(composedMessages)
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msg)"
case let .apiCreateChatItem(noteFolderId, file, mc):
let msg = encodeJSON(ComposedMessage(fileSource: file, msgContent: mc))
return "/_create *\(noteFolderId) json \(msg)"
return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
case let .apiCreateChatItems(noteFolderId, composedMessages):
let msgs = encodeJSON(composedMessages)
return "/_create *\(noteFolderId) json \(msgs)"
case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)"
case let .apiDeleteChatItem(type, id, itemIds, mode): return "/_delete item \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)"
case let .apiDeleteMemberChatItem(groupId, itemIds): return "/_delete member item #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
case let .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId, ttl):
case let .apiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl):
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId) ttl=\(ttlStr)"
return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)"
case .apiGetNtfToken: return "/_ntf get "
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
@@ -262,6 +263,7 @@ public enum ChatCommand {
case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)"
case let .apiAddContact(userId, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))"
case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))"
case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)"
case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)"
case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)"
case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)"
@@ -347,14 +349,14 @@ public enum ChatCommand {
case .apiGetChats: return "apiGetChats"
case .apiGetChat: return "apiGetChat"
case .apiGetChatItemInfo: return "apiGetChatItemInfo"
case .apiSendMessage: return "apiSendMessage"
case .apiCreateChatItem: return "apiCreateChatItem"
case .apiSendMessages: return "apiSendMessages"
case .apiCreateChatItems: return "apiCreateChatItems"
case .apiUpdateChatItem: return "apiUpdateChatItem"
case .apiDeleteChatItem: return "apiDeleteChatItem"
case .apiConnectContactViaAddress: return "apiConnectContactViaAddress"
case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem"
case .apiChatItemReaction: return "apiChatItemReaction"
case .apiForwardChatItem: return "apiForwardChatItem"
case .apiForwardChatItems: return "apiForwardChatItems"
case .apiGetNtfToken: return "apiGetNtfToken"
case .apiRegisterToken: return "apiRegisterToken"
case .apiVerifyToken: return "apiVerifyToken"
@@ -403,6 +405,7 @@ public enum ChatCommand {
case .apiVerifyGroupMember: return "apiVerifyGroupMember"
case .apiAddContact: return "apiAddContact"
case .apiSetConnectionIncognito: return "apiSetConnectionIncognito"
case .apiChangeConnectionUser: return "apiChangeConnectionUser"
case .apiConnectPlan: return "apiConnectPlan"
case .apiConnect: return "apiConnect"
case .apiDeleteChat: return "apiDeleteChat"
@@ -537,7 +540,7 @@ public enum ChatResponse: Decodable, Error {
case networkConfig(networkConfig: NetCfg)
case contactInfo(user: UserRef, contact: Contact, connectionStats_: ConnectionStats?, customUserProfile: Profile?)
case groupMemberInfo(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?)
case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: ServerQueueInfo)
case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: QueueInfo)
case contactSwitchStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats)
case groupMemberSwitchStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats)
case contactSwitchAborted(user: UserRef, contact: Contact, connectionStats: ConnectionStats)
@@ -555,6 +558,7 @@ public enum ChatResponse: Decodable, Error {
case connectionVerified(user: UserRef, verified: Bool, expectedCode: String)
case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection)
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef)
case connectionPlan(user: UserRef, connectionPlan: ConnectionPlan)
case sentConfirmation(user: UserRef, connection: PendingContactConnection)
case sentInvitation(user: UserRef, connection: PendingContactConnection)
@@ -588,7 +592,7 @@ public enum ChatResponse: Decodable, Error {
case memberSubErrors(user: UserRef, memberSubErrors: [MemberSubError])
case groupEmpty(user: UserRef, groupInfo: GroupInfo)
case userContactLinkSubscribed
case newChatItem(user: UserRef, chatItem: AChatItem)
case newChatItems(user: UserRef, chatItems: [AChatItem])
case chatItemStatusUpdated(user: UserRef, chatItem: AChatItem)
case chatItemUpdated(user: UserRef, chatItem: AChatItem)
case chatItemNotChanged(user: UserRef, chatItem: AChatItem)
@@ -725,6 +729,7 @@ public enum ChatResponse: Decodable, Error {
case .connectionVerified: return "connectionVerified"
case .invitation: return "invitation"
case .connectionIncognitoUpdated: return "connectionIncognitoUpdated"
case .connectionUserChanged: return "connectionUserChanged"
case .connectionPlan: return "connectionPlan"
case .sentConfirmation: return "sentConfirmation"
case .sentInvitation: return "sentInvitation"
@@ -758,7 +763,7 @@ public enum ChatResponse: Decodable, Error {
case .memberSubErrors: return "memberSubErrors"
case .groupEmpty: return "groupEmpty"
case .userContactLinkSubscribed: return "userContactLinkSubscribed"
case .newChatItem: return "newChatItem"
case .newChatItems: return "newChatItems"
case .chatItemStatusUpdated: return "chatItemStatusUpdated"
case .chatItemUpdated: return "chatItemUpdated"
case .chatItemNotChanged: return "chatItemNotChanged"
@@ -893,6 +898,7 @@ public enum ChatResponse: Decodable, Error {
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
case let .invitation(u, connReqInvitation, connection): return withUser(u, "connReqInvitation: \(connReqInvitation)\nconnection: \(connection)")
case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
case let .connectionUserChanged(u, fromConnection, toConnection, newUser): return withUser(u, "fromConnection: \(String(describing: fromConnection))\ntoConnection: \(String(describing: toConnection))\newUserId: \(String(describing: newUser.userId))")
case let .connectionPlan(u, connectionPlan): return withUser(u, String(describing: connectionPlan))
case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection))
case let .sentInvitation(u, connection): return withUser(u, String(describing: connection))
@@ -926,7 +932,9 @@ public enum ChatResponse: Decodable, Error {
case let .memberSubErrors(u, memberSubErrors): return withUser(u, String(describing: memberSubErrors))
case let .groupEmpty(u, groupInfo): return withUser(u, String(describing: groupInfo))
case .userContactLinkSubscribed: return noDetails
case let .newChatItem(u, chatItem): return withUser(u, String(describing: chatItem))
case let .newChatItems(u, chatItems):
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
return withUser(u, itemsString)
case let .chatItemStatusUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
case let .chatItemNotChanged(u, chatItem): return withUser(u, String(describing: chatItem))
@@ -1094,12 +1102,12 @@ public enum GroupLinkPlan: Decodable, Hashable {
case known(groupInfo: GroupInfo)
}
struct NewUser: Encodable {
struct NewUser: Encodable, Hashable {
var profile: Profile?
var pastTimestamp: Bool
}
public enum ChatPagination {
public enum ChatPagination: Hashable {
case last(count: Int)
case after(chatItemId: Int64, count: Int)
case before(chatItemId: Int64, count: Int)
@@ -1113,10 +1121,16 @@ public enum ChatPagination {
}
}
struct ComposedMessage: Encodable {
var fileSource: CryptoFile?
public struct ComposedMessage: Encodable {
public var fileSource: CryptoFile?
var quotedItemId: Int64?
var msgContent: MsgContent
public init(fileSource: CryptoFile? = nil, quotedItemId: Int64? = nil, msgContent: MsgContent) {
self.fileSource = fileSource
self.quotedItemId = quotedItemId
self.msgContent = msgContent
}
}
public struct ArchiveConfig: Encodable {
@@ -1315,7 +1329,7 @@ public struct ServerAddress: Decodable {
)
}
public struct NetCfg: Codable, Equatable {
public struct NetCfg: Codable, Equatable, Hashable {
public var socksProxy: String? = nil
var socksMode: SocksMode = .always
public var hostMode: HostMode = .publicHost
@@ -1369,18 +1383,18 @@ public struct NetCfg: Codable, Equatable {
public var enableKeepAlive: Bool { tcpKeepAlive != nil }
}
public enum HostMode: String, Codable {
public enum HostMode: String, Codable, Hashable {
case onionViaSocks
case onionHost = "onion"
case publicHost = "public"
}
public enum SocksMode: String, Codable {
public enum SocksMode: String, Codable, Hashable {
case always = "always"
case onion = "onion"
}
public enum SMPProxyMode: String, Codable, SelectableItem {
public enum SMPProxyMode: String, Codable, Hashable, SelectableItem {
case always = "always"
case unknown = "unknown"
case unprotected = "unprotected"
@@ -1400,7 +1414,7 @@ public enum SMPProxyMode: String, Codable, SelectableItem {
public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never]
}
public enum SMPProxyFallback: String, Codable, SelectableItem {
public enum SMPProxyFallback: String, Codable, Hashable, SelectableItem {
case allow = "allow"
case allowProtected = "allowProtected"
case prohibit = "prohibit"
@@ -1418,7 +1432,7 @@ public enum SMPProxyFallback: String, Codable, SelectableItem {
public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit]
}
public enum OnionHosts: String, Identifiable {
public enum OnionHosts: String, Identifiable, Hashable {
case no
case prefer
case require
@@ -1452,7 +1466,7 @@ public enum OnionHosts: String, Identifiable {
public static let values: [OnionHosts] = [.no, .prefer, .require]
}
public enum TransportSessionMode: String, Codable, Identifiable {
public enum TransportSessionMode: String, Codable, Identifiable, Hashable {
case user
case entity
@@ -1468,7 +1482,7 @@ public enum TransportSessionMode: String, Codable, Identifiable {
public static let values: [TransportSessionMode] = [.user, .entity]
}
public struct KeepAliveOpts: Codable, Equatable {
public struct KeepAliveOpts: Codable, Equatable, Hashable {
public var keepIdle: Int // seconds
public var keepIntvl: Int // seconds
public var keepCnt: Int // times
@@ -1476,7 +1490,7 @@ public struct KeepAliveOpts: Codable, Equatable {
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
}
public enum NetworkStatus: Decodable, Equatable {
public enum NetworkStatus: Decodable, Equatable, Hashable {
case unknown
case connected
case disconnected
@@ -1514,7 +1528,7 @@ public enum NetworkStatus: Decodable, Equatable {
}
}
public struct ConnNetworkStatus: Decodable {
public struct ConnNetworkStatus: Decodable, Hashable {
public var agentConnId: String
public var networkStatus: NetworkStatus
}
@@ -1539,7 +1553,7 @@ public enum MsgFilter: String, Codable, Hashable {
case mentions
}
public struct UserMsgReceiptSettings: Codable {
public struct UserMsgReceiptSettings: Codable, Hashable {
public var enable: Bool
public var clearOverrides: Bool
@@ -1588,7 +1602,7 @@ public enum SndSwitchStatus: String, Codable, Hashable {
case sendingQTEST = "sending_qtest"
}
public enum QueueDirection: String, Decodable {
public enum QueueDirection: String, Decodable, Hashable {
case rcv
case snd
}
@@ -1643,12 +1657,12 @@ public struct AutoAccept: Codable, Hashable {
}
}
public protocol SelectableItem: Identifiable, Equatable {
public protocol SelectableItem: Hashable, Identifiable {
var label: LocalizedStringKey { get }
static var values: [Self] { get }
}
public struct DeviceToken: Decodable {
public struct DeviceToken: Decodable, Hashable {
var pushProvider: PushProvider
var token: String
@@ -1662,12 +1676,12 @@ public struct DeviceToken: Decodable {
}
}
public enum PushEnvironment: String {
public enum PushEnvironment: String, Hashable {
case development
case production
}
public enum PushProvider: String, Decodable {
public enum PushProvider: String, Decodable, Hashable {
case apns_dev
case apns_prod
@@ -1681,7 +1695,7 @@ public enum PushProvider: String, Decodable {
// This notification mode is for app core, UI uses AppNotificationsMode.off to mean completely disable,
// and .local for periodic background checks
public enum NotificationsMode: String, Decodable, SelectableItem {
public enum NotificationsMode: String, Decodable, SelectableItem, Hashable {
case off = "OFF"
case periodic = "PERIODIC"
case instant = "INSTANT"
@@ -1699,7 +1713,7 @@ public enum NotificationsMode: String, Decodable, SelectableItem {
public static var values: [NotificationsMode] = [.instant, .periodic, .off]
}
public enum NotificationPreviewMode: String, SelectableItem, Codable {
public enum NotificationPreviewMode: String, SelectableItem, Codable, Hashable {
case hidden
case contact
case message
@@ -1717,7 +1731,7 @@ public enum NotificationPreviewMode: String, SelectableItem, Codable {
public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden]
}
public struct RemoteCtrlInfo: Decodable {
public struct RemoteCtrlInfo: Decodable, Hashable {
public var remoteCtrlId: Int64
public var ctrlDeviceName: String
public var sessionState: RemoteCtrlSessionState?
@@ -1727,7 +1741,7 @@ public struct RemoteCtrlInfo: Decodable {
}
}
public enum RemoteCtrlSessionState: Decodable {
public enum RemoteCtrlSessionState: Decodable, Hashable {
case starting
case searching
case connecting
@@ -1742,17 +1756,17 @@ public enum RemoteCtrlStopReason: Decodable {
case disconnected
}
public struct CtrlAppInfo: Decodable {
public struct CtrlAppInfo: Decodable, Hashable {
public var appVersionRange: AppVersionRange
public var deviceName: String
}
public struct AppVersionRange: Decodable {
public struct AppVersionRange: Decodable, Hashable {
public var minVersion: String
public var maxVersion: String
}
public struct CoreVersionInfo: Decodable {
public struct CoreVersionInfo: Decodable, Hashable {
public var version: String
public var simplexmqVersion: String
public var simplexmqCommit: String
@@ -1842,7 +1856,6 @@ public enum ChatErrorType: Decodable, Hashable {
case inlineFileProhibited(fileId: Int64)
case invalidQuote
case invalidForward
case forwardNoFile
case invalidChatItemUpdate
case invalidChatItemDelete
case hasCurrentCall
@@ -1857,6 +1870,7 @@ public enum ChatErrorType: Decodable, Hashable {
case agentCommandError(message: String)
case invalidFileDescription(message: String)
case connectionIncognitoChangeProhibited
case connectionUserChangeProhibited
case peerChatVRangeIncompatible
case internalError(message: String)
case exception(message: String)
@@ -2090,14 +2104,14 @@ public enum RemoteCtrlError: Decodable, Hashable {
case protocolError
}
public struct MigrationFileLinkData: Codable {
public struct MigrationFileLinkData: Codable, Hashable {
let networkConfig: NetworkConfig?
public init(networkConfig: NetworkConfig) {
self.networkConfig = networkConfig
}
public struct NetworkConfig: Codable {
public struct NetworkConfig: Codable, Hashable {
let socksProxy: String?
let hostMode: HostMode?
let requiredHostMode: Bool?
@@ -2129,7 +2143,7 @@ public struct MigrationFileLinkData: Codable {
}
}
public struct AppSettings: Codable, Equatable {
public struct AppSettings: Codable, Equatable, Hashable {
public var networkConfig: NetCfg? = nil
public var privacyEncryptLocalFiles: Bool? = nil
public var privacyAskToApproveRelays: Bool? = nil
@@ -2224,7 +2238,7 @@ public struct AppSettings: Codable, Equatable {
}
}
public enum AppSettingsNotificationMode: String, Codable {
public enum AppSettingsNotificationMode: String, Codable, Hashable {
case off
case periodic
case instant
@@ -2252,13 +2266,13 @@ public enum AppSettingsNotificationMode: String, Codable {
// case message
//}
public enum AppSettingsLockScreenCalls: String, Codable {
public enum AppSettingsLockScreenCalls: String, Codable, Hashable {
case disable
case show
case accept
}
public struct UserNetworkInfo: Codable, Equatable {
public struct UserNetworkInfo: Codable, Equatable, Hashable {
public let networkType: UserNetworkType
public let online: Bool
@@ -2268,7 +2282,7 @@ public struct UserNetworkInfo: Codable, Equatable {
}
}
public enum UserNetworkType: String, Codable {
public enum UserNetworkType: String, Codable, Hashable {
case none
case cellular
case wifi
@@ -2286,7 +2300,7 @@ public enum UserNetworkType: String, Codable {
}
}
public struct RcvMsgInfo: Codable {
public struct RcvMsgInfo: Codable, Hashable {
var msgId: Int64
var msgDeliveryId: Int64
var msgDeliveryStatus: String
@@ -2294,16 +2308,7 @@ public struct RcvMsgInfo: Codable {
var agentMsgMeta: String
}
public struct ServerQueueInfo: Codable {
var server: String
var rcvId: String
var sndId: String
var ntfId: String?
var status: String
var info: QueueInfo
}
public struct QueueInfo: Codable {
public struct QueueInfo: Codable, Hashable {
var qiSnd: Bool
var qiNtf: Bool
var qiSub: QSub?
@@ -2311,25 +2316,25 @@ public struct QueueInfo: Codable {
var qiMsg: MsgInfo?
}
public struct QSub: Codable {
public struct QSub: Codable, Hashable {
var qSubThread: QSubThread
var qDelivered: String?
}
public enum QSubThread: String, Codable {
public enum QSubThread: String, Codable, Hashable {
case noSub
case subPending
case subThread
case prohibitSub
}
public struct MsgInfo: Codable {
public struct MsgInfo: Codable, Hashable {
var msgId: String
var msgTs: Date
var msgType: MsgType
}
public enum MsgType: String, Codable {
public enum MsgType: String, Codable, Hashable {
case message
case quota
}
+2
View File
@@ -13,6 +13,8 @@ public let appSuspendTimeout: Int = 15 // seconds
public let defaultProfileImageCorner: Double = 22.5
public let defaultChatItemRoundness: Double = 0.8
let GROUP_DEFAULT_APP_STATE = "appState"
let GROUP_DEFAULT_NSE_STATE = "nseState"
let GROUP_DEFAULT_SE_STATE = "seState"
+3 -2
View File
@@ -42,7 +42,6 @@ public struct RcvCallInvitation: Decodable {
public var contact: Contact
public var callType: CallType
public var sharedKey: String?
public var callUUID: String?
public var callTs: Date
public var callTypeText: LocalizedStringKey {
get {
@@ -53,8 +52,10 @@ public struct RcvCallInvitation: Decodable {
}
}
public var callkitUUID: UUID? = UUID()
private enum CodingKeys: String, CodingKey {
case user, contact, callType, sharedKey, callUUID, callTs
case user, contact, callType, sharedKey, callTs
}
public static let sampleData = RcvCallInvitation(
+1 -1
View File
@@ -2762,7 +2762,7 @@ let msgTimeFormat = Date.FormatStyle.dateTime.hour().minute()
let msgDateFormat = Date.FormatStyle.dateTime.day(.twoDigits).month(.twoDigits)
public func formatTimestampText(_ date: Date) -> Text {
return Text(date, format: recent(date) ? msgTimeFormat : msgDateFormat)
Text(verbatim: date.formatted(recent(date) ? msgTimeFormat : msgDateFormat))
}
private func recent(_ date: Date) -> Bool {
@@ -424,7 +424,6 @@ fun PreviewIncomingCallLockScreenAlert() {
) {
IncomingCallLockScreenAlertLayout(
invitation = RcvCallInvitation(
callUUID = "",
remoteHostId = null,
user = User.sampleData,
contact = Contact.sampleData,
@@ -770,7 +770,6 @@ fun PreviewActiveCallOverlayVideo() {
callState = CallState.Negotiated,
localMedia = CallMediaType.Video,
peerMedia = CallMediaType.Video,
callUUID = "",
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
RTCIceCandidate(RTCIceCandidateType.Host, "tcp")
@@ -800,7 +799,6 @@ fun PreviewActiveCallOverlayAudio() {
callState = CallState.Negotiated,
localMedia = CallMediaType.Audio,
peerMedia = CallMediaType.Audio,
callUUID = "",
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
RTCIceCandidate(RTCIceCandidateType.Host, "udp")
@@ -14,7 +14,6 @@ actual fun authenticate(
promptSubtitle: String,
selfDestruct: Boolean,
usingLAMode: LAMode,
oneTime: Boolean,
completed: (LAResult) -> Unit
) {
val activity = mainActivity.get() ?: return completed(LAResult.Error(""))
@@ -28,7 +27,7 @@ actual fun authenticate(
else -> completed(LAResult.Unavailable())
}
LAMode.PASSCODE -> {
authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, oneTime, completed)
authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed)
}
}
}
@@ -14,11 +14,13 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.MaterialTheme.colors
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
@@ -31,6 +33,7 @@ import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.saveAppLocale
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.compose.painterResource
@@ -79,7 +82,7 @@ fun AppearanceScope.AppearanceLayout(
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.appearance_settings))
SectionView(stringResource(MR.strings.settings_section_title_interface), contentPadding = PaddingValues()) {
SectionView(stringResource(MR.strings.settings_section_title_interface), padding = PaddingValues()) {
val context = LocalContext.current
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// SectionItemWithValue(
@@ -120,7 +123,7 @@ fun AppearanceScope.AppearanceLayout(
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.settings_section_title_icon), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
LazyRow {
items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index ->
val item = AppIcon.values()[index]
@@ -74,7 +74,6 @@ fun MainScreen() {
LaunchedEffect(showAdvertiseLAAlert) {
if (
!chatModel.controller.appPrefs.laNoticeShown.get()
&& !appPrefs.performLA.get()
&& showAdvertiseLAAlert
&& chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete
&& chatModel.chats.size > 3
@@ -212,8 +211,10 @@ fun MainScreen() {
} else {
ActiveCallView()
}
} else {
// It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked
ModalManager.fullscreen.showPasscodeInView()
}
ModalManager.fullscreen.showOneTimePasscodeInView()
AlertManager.privacySensitive.showInView()
if (onboarding == OnboardingStage.OnboardingComplete) {
LaunchedEffect(chatModel.currentUser.value, chatModel.appOpenUrl.value) {
@@ -5,7 +5,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.localauth.SetAppPasscodeView
@@ -32,7 +31,7 @@ object AppLock {
fun showLANotice(laNoticeShown: SharedPreference<Boolean>) {
Log.d(TAG, "showLANotice")
if (!laNoticeShown.get() && !appPrefs.performLA.get()) {
if (!laNoticeShown.get()) {
laNoticeShown.set(true)
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.la_notice_title_simplex_lock),
@@ -58,8 +57,6 @@ object AppLock {
private fun showChooseLAMode() {
Log.d(TAG, "showLANotice")
if (appPrefs.performLA.get()) return
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.la_lock_mode),
text = null,
@@ -83,23 +80,21 @@ object AppLock {
authenticate(
generalGetString(MR.strings.auth_enable_simplex_lock),
generalGetString(MR.strings.auth_confirm_credential),
oneTime = true,
completed = { laResult ->
when (laResult) {
LAResult.Success -> {
m.showAuthScreen.value = true
m.performLA.value = true
appPrefs.performLA.set(true)
laTurnedOnAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.showAuthScreen.value = false
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
// appPrefs.performLA.set(false)
m.performLA.value = false
appPrefs.performLA.set(false)
laFailedAlert()
}
is LAResult.Unavailable -> {
m.showAuthScreen.value = false
m.performLA.value = false
appPrefs.performLA.set(false)
m.showAdvertiseLAUnavailableAlert.value = true
}
@@ -109,22 +104,19 @@ object AppLock {
}
private fun setPasscode() {
if (appPrefs.performLA.get()) return
val appPrefs = ChatController.appPrefs
ModalManager.fullscreen.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) {
SetAppPasscodeView(
submit = {
ChatModel.showAuthScreen.value = true
ChatModel.performLA.value = true
appPrefs.performLA.set(true)
appPrefs.laMode.set(LAMode.PASSCODE)
laTurnedOnAlert()
},
cancel = {
ChatModel.showAuthScreen.value = false
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
// appPrefs.performLA.set(false)
ChatModel.performLA.value = false
appPrefs.performLA.set(false)
laPasscodeNotSetAlert()
},
close = close
@@ -155,7 +147,6 @@ object AppLock {
else
generalGetString(MR.strings.auth_unlock),
selfDestruct = true,
oneTime = false,
completed = { laResult ->
when (laResult) {
LAResult.Success ->
@@ -169,7 +160,7 @@ object AppLock {
}
is LAResult.Unavailable -> {
userAuthorized.value = true
m.showAuthScreen.value = false
m.performLA.value = false
m.controller.appPrefs.performLA.set(false)
laUnavailableTurningOffAlert()
}
@@ -201,23 +192,22 @@ object AppLock {
generalGetString(MR.strings.auth_confirm_credential)
else
"",
oneTime = true,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) {
LAResult.Success -> {
m.showAuthScreen.value = true
m.performLA.value = true
prefPerformLA.set(true)
laTurnedOnAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.showAuthScreen.value = false
m.performLA.value = false
prefPerformLA.set(false)
laFailedAlert()
}
is LAResult.Unavailable -> {
m.showAuthScreen.value = false
m.performLA.value = false
prefPerformLA.set(false)
laUnavailableInstructionAlert()
}
@@ -237,13 +227,12 @@ object AppLock {
generalGetString(MR.strings.auth_confirm_credential)
else
generalGetString(MR.strings.auth_disable_simplex_lock),
oneTime = true,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
val selfDestructPref = m.controller.appPrefs.selfDestruct
when (laResult) {
LAResult.Success -> {
m.showAuthScreen.value = false
m.performLA.value = false
prefPerformLA.set(false)
DatabaseUtils.ksAppPassword.remove()
selfDestructPref.set(false)
@@ -251,12 +240,12 @@ object AppLock {
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.showAuthScreen.value = true
m.performLA.value = true
prefPerformLA.set(true)
laFailedAlert()
}
is LAResult.Unavailable -> {
m.showAuthScreen.value = false
m.performLA.value = false
prefPerformLA.set(false)
laUnavailableTurningOffAlert()
}
@@ -20,6 +20,7 @@ import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.internal.ChannelFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.*
@@ -97,7 +98,7 @@ object ChatModel {
}
)
}
val showAuthScreen by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) }
val performLA by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) }
val showAdvertiseLAUnavailableAlert = mutableStateOf(false)
val showChatPreviews by lazy { mutableStateOf(ChatController.appPrefs.privacyShowChatPreviews.get()) }
@@ -984,7 +985,6 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val fullName get() = contact.fullName
override val image get() = contact.image
override val localAlias: String get() = contact.localAlias
override fun anyNameContains(searchAnyCase: String): Boolean = contact.anyNameContains(searchAnyCase)
companion object {
val sampleData = Direct(Contact.sampleData)
@@ -1219,12 +1219,6 @@ data class Contact(
override val localAlias get() = profile.localAlias
val verified get() = activeConn?.connectionCode != null
override fun anyNameContains(searchAnyCase: String): Boolean {
val s = searchAnyCase.trim().lowercase()
return profile.chatViewName.lowercase().contains(s) || profile.displayName.lowercase().contains(s) || profile.fullName.lowercase().contains(s)
}
val directOrUsed: Boolean get() =
if (activeConn != null) {
(activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed
@@ -846,15 +846,15 @@ object ChatController {
return null
}
suspend fun apiSendMessage(rh: Long?, type: ChatType, id: Long, file: CryptoFile? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? {
val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live, ttl)
suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? {
val cmd = CC.ApiSendMessages(type, id, live, ttl, composedMessages)
return processSendMessageCmd(rh, cmd)
}
private suspend fun processSendMessageCmd(rh: Long?, cmd: CC): AChatItem? {
private suspend fun processSendMessageCmd(rh: Long?, cmd: CC): List<AChatItem>? {
val r = sendCmd(rh, cmd)
return when (r) {
is CR.NewChatItem -> r.chatItem
is CR.NewChatItems -> r.chatItems
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("processSendMessageCmd", generalGetString(MR.strings.error_sending_message), r)
@@ -863,13 +863,13 @@ object ChatController {
}
}
}
suspend fun apiCreateChatItem(rh: Long?, noteFolderId: Long, file: CryptoFile? = null, mc: MsgContent): AChatItem? {
val cmd = CC.ApiCreateChatItem(noteFolderId, file, mc)
suspend fun apiCreateChatItems(rh: Long?, noteFolderId: Long, composedMessages: List<ComposedMessage>): List<AChatItem>? {
val cmd = CC.ApiCreateChatItems(noteFolderId, composedMessages)
val r = sendCmd(rh, cmd)
return when (r) {
is CR.NewChatItem -> r.chatItem
is CR.NewChatItems -> r.chatItems
else -> {
apiErrorAlert("apiCreateChatItem", generalGetString(MR.strings.error_creating_message), r)
apiErrorAlert("apiCreateChatItems", generalGetString(MR.strings.error_creating_message), r)
null
}
}
@@ -885,9 +885,9 @@ object ChatController {
}
}
suspend fun apiForwardChatItem(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemId: Long, ttl: Int?): ChatItem? {
val cmd = CC.ApiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId, ttl)
return processSendMessageCmd(rh, cmd)?.chatItem
suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemIds: List<Long>, ttl: Int?): List<ChatItem>? {
val cmd = CC.ApiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl)
return processSendMessageCmd(rh, cmd)?.map { it.chatItem }
}
@@ -1030,14 +1030,14 @@ object ChatController {
return null
}
suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair<RcvMsgInfo?, ServerQueueInfo>? {
suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair<RcvMsgInfo?, QueueInfo>? {
val r = sendCmd(rh, CC.APIContactQueueInfo(contactId))
if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo)
apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r)
return null
}
suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair<RcvMsgInfo?, ServerQueueInfo>? {
suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair<RcvMsgInfo?, QueueInfo>? {
val r = sendCmd(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId))
if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo)
apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r)
@@ -2132,27 +2132,30 @@ object ChatController {
chatModel.networkStatuses[s.agentConnId] = s.networkStatus
}
}
is CR.NewChatItem -> withBGApi {
val cInfo = r.chatItem.chatInfo
val cItem = r.chatItem.chatItem
if (active(r.user)) {
withChats {
addChatItem(rhId, cInfo, cItem)
is CR.NewChatItems -> withBGApi {
r.chatItems.forEach { chatItem ->
val cInfo = chatItem.chatInfo
val cItem = chatItem.chatItem
if (active(r.user)) {
withChats {
addChatItem(rhId, cInfo, cItem)
}
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
chatModel.increaseUnreadCounter(rhId, r.user)
}
} else if (cItem.isRcvNew && cInfo.ntfsEnabled) {
chatModel.increaseUnreadCounter(rhId, r.user)
}
val file = cItem.file
val mc = cItem.content.msgContent
if (file != null &&
val file = cItem.file
val mc = cItem.content.msgContent
if (file != null &&
appPrefs.privacyAcceptImages.get() &&
((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
|| (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
receiveFile(rhId, r.user, file.fileId, auto = true)
}
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) {
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))
) {
receiveFile(rhId, r.user, file.fileId, auto = true)
}
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) {
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
}
}
}
is CR.ChatItemStatusUpdated -> {
@@ -2863,13 +2866,13 @@ sealed class CC {
class ApiGetChats(val userId: Long): CC()
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
class ApiSendMessage(val type: ChatType, val id: Long, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC()
class ApiCreateChatItem(val noteFolderId: Long, val file: CryptoFile?, val mc: MsgContent): CC()
class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
class ApiCreateChatItems(val noteFolderId: Long, val composedMessages: List<ComposedMessage>): CC()
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List<Long>): CC()
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
class ApiForwardChatItem(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemId: Long, val ttl: Int?): CC()
class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemIds: List<Long>, val ttl: Int?): CC()
class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC()
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
class ApiJoinGroup(val groupId: Long): CC()
@@ -3008,20 +3011,22 @@ sealed class CC {
is ApiGetChats -> "/_get chats $userId pcc=on"
is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
is ApiSendMessage -> {
is ApiSendMessages -> {
val msgs = json.encodeToString(composedMessages)
val ttlStr = if (ttl != null) "$ttl" else "default"
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs"
}
is ApiCreateChatItem -> {
"/_create *$noteFolderId json ${json.encodeToString(ComposedMessage(file, null, mc))}"
is ApiCreateChatItems -> {
val msgs = json.encodeToString(composedMessages)
"/_create *$noteFolderId json $msgs"
}
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}"
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} ${itemIds.joinToString(",")} ${mode.deleteMode}"
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId ${itemIds.joinToString(",")}"
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
is ApiForwardChatItem -> {
is ApiForwardChatItems -> {
val ttlStr = if (ttl != null) "$ttl" else "default"
"/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} $itemId ttl=${ttlStr}"
"/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
}
is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}"
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
@@ -3158,13 +3163,13 @@ sealed class CC {
is ApiGetChats -> "apiGetChats"
is ApiGetChat -> "apiGetChat"
is ApiGetChatItemInfo -> "apiGetChatItemInfo"
is ApiSendMessage -> "apiSendMessage"
is ApiCreateChatItem -> "apiCreateChatItem"
is ApiSendMessages -> "apiSendMessages"
is ApiCreateChatItems -> "apiCreateChatItems"
is ApiUpdateChatItem -> "apiUpdateChatItem"
is ApiDeleteChatItem -> "apiDeleteChatItem"
is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem"
is ApiChatItemReaction -> "apiChatItemReaction"
is ApiForwardChatItem -> "apiForwardChatItem"
is ApiForwardChatItems -> "apiForwardChatItems"
is ApiNewGroup -> "apiNewGroup"
is ApiAddMember -> "apiAddMember"
is ApiJoinGroup -> "apiJoinGroup"
@@ -4734,7 +4739,7 @@ sealed class CR {
@Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR()
@Serializable @SerialName("contactInfo") class ContactInfo(val user: UserRef, val contact: Contact, val connectionStats_: ConnectionStats? = null, val customUserProfile: Profile? = null): CR()
@Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats? = null): CR()
@Serializable @SerialName("queueInfo") class QueueInfoR(val user: UserRef, val rcvMsgInfo: RcvMsgInfo?, val queueInfo: ServerQueueInfo): CR()
@Serializable @SerialName("queueInfo") class QueueInfoR(val user: UserRef, val rcvMsgInfo: RcvMsgInfo?, val queueInfo: QueueInfo): CR()
@Serializable @SerialName("contactSwitchStarted") class ContactSwitchStarted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR()
@Serializable @SerialName("groupMemberSwitchStarted") class GroupMemberSwitchStarted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR()
@Serializable @SerialName("contactSwitchAborted") class ContactSwitchAborted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR()
@@ -4790,7 +4795,7 @@ sealed class CR {
@Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: UserRef, val memberSubErrors: List<MemberSubError>): CR()
@Serializable @SerialName("groupEmpty") class GroupEmpty(val user: UserRef, val group: GroupInfo): CR()
@Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR()
@Serializable @SerialName("newChatItem") class NewChatItem(val user: UserRef, val chatItem: AChatItem): CR()
@Serializable @SerialName("newChatItems") class NewChatItems(val user: UserRef, val chatItems: List<AChatItem>): CR()
@Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val user: UserRef, val chatItem: AChatItem): CR()
@Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR()
@Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR()
@@ -4966,7 +4971,7 @@ sealed class CR {
is MemberSubErrors -> "memberSubErrors"
is GroupEmpty -> "groupEmpty"
is UserContactLinkSubscribed -> "userContactLinkSubscribed"
is NewChatItem -> "newChatItem"
is NewChatItems -> "newChatItems"
is ChatItemStatusUpdated -> "chatItemStatusUpdated"
is ChatItemUpdated -> "chatItemUpdated"
is ChatItemNotChanged -> "chatItemNotChanged"
@@ -5134,7 +5139,7 @@ sealed class CR {
is MemberSubErrors -> withUser(user, json.encodeToString(memberSubErrors))
is GroupEmpty -> withUser(user, json.encodeToString(group))
is UserContactLinkSubscribed -> noDetails()
is NewChatItem -> withUser(user, json.encodeToString(chatItem))
is NewChatItems -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) })
is ChatItemStatusUpdated -> withUser(user, json.encodeToString(chatItem))
is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem))
is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem))
@@ -6409,16 +6414,6 @@ data class RcvMsgInfo (
val agentMsgMeta: String
)
@Serializable
data class ServerQueueInfo (
val server: String,
val rcvId: String,
val sndId: String,
val ntfId: String? = null,
val status: String,
val info: QueueInfo
)
@Serializable
data class QueueInfo (
val qiSnd: Boolean,
@@ -47,7 +47,6 @@ class CallManager(val chatModel: ChatModel) {
remoteHostId = invitation.remoteHostId,
userProfile = userProfile,
contact = invitation.contact,
callUUID = invitation.callUUID,
callState = CallState.InvitationAccepted,
localMedia = invitation.callType.media,
sharedKey = invitation.sharedKey,
@@ -115,7 +115,6 @@ fun PreviewIncomingCallAlertLayout() {
contact = Contact.sampleData,
callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)),
sharedKey = null,
callUUID = "",
callTs = Clock.System.now()
),
chatModel = ChatModel,
@@ -13,7 +13,6 @@ data class Call(
val remoteHostId: Long?,
val userProfile: Profile,
val contact: Contact,
val callUUID: String?,
val callState: CallState,
val localMedia: CallMediaType,
val localCapabilities: CallCapabilities? = null,
@@ -106,7 +105,6 @@ sealed class WCallResponse {
val contact: Contact,
val callType: CallType,
val sharedKey: String? = null,
val callUUID: String,
val callTs: Instant
) {
val callTypeText: String get() = generalGetString(when(callType.media) {
@@ -1268,7 +1268,7 @@ fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) {
)
}
fun queueInfoText(info: Pair<RcvMsgInfo?, ServerQueueInfo>): String {
fun queueInfoText(info: Pair<RcvMsgInfo?, QueueInfo>): String {
val (rcvMsgInfo, qInfo) = info
val msgInfo: String = if (rcvMsgInfo != null) json.encodeToString(rcvMsgInfo) else generalGetString(MR.strings.message_queue_info_none)
return generalGetString(MR.strings.message_queue_info_server_info).format(json.encodeToString(qInfo), msgInfo)
@@ -282,14 +282,14 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
val versions = ciInfo.itemVersions
if (versions.isNotEmpty()) {
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.edit_history), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
versions.forEachIndexed { i, ciVersion ->
ItemVersionView(ciVersion, current = i == 0)
}
}
} else {
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(MR.strings.no_history), color = MaterialTheme.colors.secondary)
}
@@ -304,7 +304,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
Details()
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.in_reply_to), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
QuotedMsgView(qi)
}
@@ -381,14 +381,14 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true)
val mss = membersStatuses(chatModel, memberDeliveryStatuses)
if (mss.isNotEmpty()) {
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
mss.forEach { (member, status, sentViaProxy) ->
MemberDeliveryStatusView(member, status, sentViaProxy)
}
}
} else {
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(MR.strings.no_info_on_delivery), color = MaterialTheme.colors.secondary)
}
@@ -86,7 +86,6 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
.collect { chatId ->
markUnreadChatAsRead(chatId)
showSearch.value = false
searchText.value = ""
selectedChatItems.value = null
}
}
@@ -545,7 +544,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
if (chatInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId)
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
chatModel.showCallView.value = true
chatModel.callCommand.add(WCallCommand.Capabilities(media))
}
@@ -380,24 +380,28 @@ fun ComposeView(
suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {
val cInfo = chat.chatInfo
val aChatItem = if (chat.chatInfo.chatType == ChatType.Local)
chatModel.controller.apiCreateChatItem(rh = chat.remoteHostId, noteFolderId = chat.chatInfo.apiId, file = file, mc = mc)
val chatItems = if (chat.chatInfo.chatType == ChatType.Local)
chatModel.controller.apiCreateChatItems(
rh = chat.remoteHostId,
noteFolderId = chat.chatInfo.apiId,
composedMessages = listOf(ComposedMessage(file, null, mc))
)
else
chatModel.controller.apiSendMessage(
rh = chat.remoteHostId,
type = cInfo.chatType,
id = cInfo.apiId,
file = file,
quotedItemId = quoted,
mc = mc,
live = live,
ttl = ttl
)
if (aChatItem != null) {
withChats {
addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
chatModel.controller.apiSendMessages(
rh = chat.remoteHostId,
type = cInfo.chatType,
id = cInfo.apiId,
live = live,
ttl = ttl,
composedMessages = listOf(ComposedMessage(file, quoted, mc))
)
if (!chatItems.isNullOrEmpty()) {
chatItems.forEach { aChatItem ->
withChats {
addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
}
}
return aChatItem.chatItem
return chatItems.first().chatItem
}
if (file != null) removeFile(file.filePath)
return null
@@ -414,21 +418,22 @@ fun ComposeView(
}
suspend fun forwardItem(rhId: Long?, forwardedItem: ChatItem, fromChatInfo: ChatInfo, ttl: Int?): ChatItem? {
val chatItem = controller.apiForwardChatItem(
val chatItems = controller.apiForwardChatItems(
rh = rhId,
toChatType = chat.chatInfo.chatType,
toChatId = chat.chatInfo.apiId,
fromChatType = fromChatInfo.chatType,
fromChatId = fromChatInfo.apiId,
itemId = forwardedItem.id,
itemIds = listOf(forwardedItem.id),
ttl = ttl
)
if (chatItem != null) {
chatItems?.forEach { chatItem ->
withChats {
addChatItem(rhId, chat.chatInfo, chatItem)
}
}
return chatItem
// TODO batch send: forward multiple messages
return chatItems?.firstOrNull()
}
fun checkLinkPreview(): MsgContent {
@@ -519,6 +524,7 @@ fun ComposeView(
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
is ComposePreview.MediaPreview -> {
// TODO batch send: batch media previews
preview.content.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.SimpleImage ->
@@ -509,11 +509,11 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() -
verticalAlignment = Alignment.CenterVertically
) {
Row(
Modifier.weight(1f).padding(top = 8.dp, end = DEFAULT_PADDING, bottom = 8.dp),
Modifier.weight(1f).padding(end = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
MemberProfileImage(size = 42.dp, member)
MemberProfileImage(size = DEFAULT_MIN_SECTION_ITEM_HEIGHT, member)
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -80,7 +80,7 @@ private fun ShareListNavLinkLayout(
click: () -> Unit,
stopped: Boolean,
) {
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = 8.dp), click = click, disabled = stopped) {
SectionItemView(minHeight = 50.dp, click = click, disabled = stopped) {
chatLinkPreview()
}
Divider(Modifier.padding(horizontal = 8.dp))
@@ -98,11 +98,9 @@ private fun SharePreviewView(chat: Chat, disabled: Boolean) {
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
if (chat.chatInfo is ChatInfo.Local) {
ProfileImage(size = 42.dp, null, icon = MR.images.ic_folder_filled, color = NoteFolderIconColor)
} else if (chat.chatInfo is ChatInfo.Group) {
ProfileImage(size = 42.dp, chat.chatInfo.image, icon = MR.images.ic_supervised_user_circle_filled)
ProfileImage(size = 46.dp, null, icon = MR.images.ic_folder_filled, color = NoteFolderIconColor)
} else {
ProfileImage(size = 42.dp, chat.chatInfo.image)
ProfileImage(size = 46.dp, chat.chatInfo.image)
}
Text(
chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
@@ -94,17 +94,10 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
}
if (appPlatform.isAndroid) {
tryOrShowError("UserPicker", error = {}) {
UserPicker(
chatModel,
userPickerState,
showSettings = false,
showCancel = true,
contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart,
cancelClicked = {
chatModel.sharedContent.value = null
userPickerState.value = AnimatedViewState.GONE
}
)
UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = {
chatModel.sharedContent.value = null
userPickerState.value = AnimatedViewState.GONE
})
}
}
}
@@ -5,7 +5,9 @@ import SectionSpacer
import SectionView
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -64,7 +66,7 @@ fun DatabaseErrorView(
Modifier.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
style = MaterialTheme.typography.h1
)
SectionView(null, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content = content)
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content)
}
@Composable
@@ -423,7 +423,6 @@ fun authStopChat(m: ChatModel, progressIndicator: MutableState<Boolean>? = null,
authenticate(
generalGetString(MR.strings.auth_stop_chat),
generalGetString(MR.strings.auth_log_in_using_credential),
oneTime = true,
completed = { laResult ->
when (laResult) {
LAResult.Success, is LAResult.Unavailable -> {
@@ -85,8 +85,7 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
Text(
title.value,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
maxLines = 1
)
}
} else {
@@ -34,7 +34,6 @@ expect fun authenticate(
promptSubtitle: String,
selfDestruct: Boolean = false,
usingLAMode: LAMode = ChatModel.controller.appPrefs.laMode.get(),
oneTime: Boolean,
completed: (LAResult) -> Unit
)
@@ -42,11 +41,10 @@ fun authenticateWithPasscode(
promptTitle: String,
promptSubtitle: String,
selfDestruct: Boolean,
oneTime: Boolean,
completed: (LAResult) -> Unit
) {
val password = DatabaseUtils.ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(MR.strings.la_no_app_password)))
ModalManager.fullscreen.showPasscodeCustomModal(oneTime) { close ->
ModalManager.fullscreen.showPasscodeCustomModal { close ->
BackHandler {
close()
completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled)))
@@ -69,7 +69,6 @@ class ModalManager(private val placement: ModalPlacement? = null) {
// Don't use mutableStateOf() here, because it produces this if showing from SimpleXAPI.startChat():
// java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied
private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null)
fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) {
val data = ModalData()
@@ -106,13 +105,9 @@ class ModalManager(private val placement: ModalPlacement? = null) {
}
}
fun showPasscodeCustomModal(oneTime: Boolean, modal: @Composable (close: () -> Unit) -> Unit) {
Log.d(TAG, "ModalManager.showPasscodeCustomModal, oneTime: $oneTime")
if (oneTime) {
onTimePasscodeView.value = modal
} else {
passcodeView.value = modal
}
fun showPasscodeCustomModal(modal: @Composable (close: () -> Unit) -> Unit) {
Log.d(TAG, "ModalManager.showPasscodeCustomModal")
passcodeView.value = modal
}
fun hasModalsOpen() = modalCount.value > 0
@@ -184,11 +179,6 @@ class ModalManager(private val placement: ModalPlacement? = null) {
passcodeView.collectAsState().value?.invoke { passcodeView.value = null }
}
@Composable
fun showOneTimePasscodeInView() {
onTimePasscodeView.collectAsState().value?.invoke { onTimePasscodeView.value = null }
}
/**
* Allows to modify a list without getting [ConcurrentModificationException]
* */
@@ -21,15 +21,15 @@ import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
import chat.simplex.res.MR
@Composable
fun SectionView(title: String? = null, contentPadding: PaddingValues = PaddingValues(), headerBottomPadding: Dp = DEFAULT_PADDING, content: (@Composable ColumnScope.() -> Unit)) {
fun SectionView(title: String? = null, padding: PaddingValues = PaddingValues(), content: (@Composable ColumnScope.() -> Unit)) {
Column {
if (title != null) {
Text(
title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2,
modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = headerBottomPadding), fontSize = 12.sp
modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = DEFAULT_PADDING), fontSize = 12.sp
)
}
Column(Modifier.padding(contentPadding).fillMaxWidth()) { content() }
Column(Modifier.padding(padding).fillMaxWidth()) { content() }
}
}
@@ -1,9 +1,7 @@
package chat.simplex.common.views.newchat
import SectionDivider
import SectionDividerSpaced
import SectionItemView
import SectionSpacer
import SectionView
import TextIconSpaced
import androidx.compose.desktop.ui.tooling.preview.Preview
@@ -166,10 +164,6 @@ private fun NewChatSheetLayout(
)
val sectionModifier = Modifier.fillMaxWidth()
val deletedContactTypes = listOf(ContactType.CHAT_DELETED)
val deletedChats by remember(chatModel.chats.value, deletedContactTypes) {
derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) }
}
LazyColumnWithScrollBar(
Modifier.fillMaxSize(),
@@ -230,9 +224,7 @@ private fun NewChatSheetLayout(
}
}
item {
if (searchText.value.text.isEmpty()) {
Spacer(Modifier.padding(bottom = 27.dp))
}
Spacer(Modifier.padding(bottom = 27.dp))
val actionButtonsOriginal = listOf(
Triple(
@@ -270,30 +262,37 @@ private fun NewChatSheetLayout(
}
}
}
val deletedContactTypes = listOf(ContactType.CHAT_DELETED)
val deletedChats by remember(chatModel.chats.value, deletedContactTypes) {
derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) }
}
if (deletedChats.isNotEmpty()) {
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
SectionItemView(
click = {
ModalManager.start.showCustomModal { closeDeletedChats ->
ModalView(
close = closeDeletedChats,
closeOnTop = !oneHandUI.value,
) {
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
ModalManager.start.closeModals()
})
Row(modifier = sectionModifier) {
SectionView {
SectionItemView(
click = {
ModalManager.start.showCustomModal { closeDeletedChats ->
ModalView(
close = closeDeletedChats,
closeOnTop = !oneHandUI.value,
) {
DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = {
ModalManager.start.closeModals()
})
}
}
}
) {
Icon(
painterResource(MR.images.ic_inventory_2),
contentDescription = stringResource(MR.strings.deleted_chats),
tint = MaterialTheme.colors.secondary,
)
TextIconSpaced(false)
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
}
) {
Icon(
painterResource(MR.images.ic_inventory_2),
contentDescription = stringResource(MR.strings.deleted_chats),
tint = MaterialTheme.colors.secondary,
)
TextIconSpaced(false)
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
}
}
}
@@ -301,28 +300,16 @@ private fun NewChatSheetLayout(
}
item {
if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) {
if (!oneHandUI.value) {
SectionDividerSpaced()
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
if (filteredContactChats.isNotEmpty() && !oneHandUI.value) {
if (searchText.value.text.isNotEmpty()) {
Spacer(Modifier.height(DEFAULT_PADDING))
} else {
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {}
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
}
}
}
item {
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Text(
generalGetString(MR.strings.no_filtered_contacts),
color = MaterialTheme.colors.secondary
)
}
SectionDividerSpaced()
}
Text(
stringResource(MR.strings.contact_list_header_title).uppercase(), color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2,
modifier = sectionModifier.padding(start = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF), fontSize = 12.sp
)
}
}
@@ -335,6 +322,17 @@ private fun NewChatSheetLayout(
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true)
}
}
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Text(
generalGetString(MR.strings.no_filtered_contacts),
color = MaterialTheme.colors.secondary
)
}
}
}
}
@Composable
@@ -557,75 +555,78 @@ private fun DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Un
}
}
}
) { contentPadding ->
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val searchShowingSimplexLink = remember { mutableStateOf(false) }
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
val showUnreadAndFavorites = remember { appPrefs.showUnreadAndFavorites.state }.value
val allChats by remember(chatModel.chats.value) {
derivedStateOf { filterContactTypes(chatModel.chats.value, listOf(ContactType.CHAT_DELETED)) }
}
val filteredContactChats = filteredContactChats(
showUnreadAndFavorites = showUnreadAndFavorites,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
searchShowingSimplexLink = searchShowingSimplexLink,
searchText = searchText.value.text,
contactChats = allChats
)
LazyColumnWithScrollBar(
Modifier.fillMaxSize(),
contentPadding = contentPadding,
reverseLayout = oneHandUI.value,
) {
Column(
Modifier
.fillMaxSize()
.padding(it)
) {
item {
if (!oneHandUI.value) {
Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING
AppBarTitle(
stringResource(MR.strings.deleted_chats),
hostDevice(rh?.remoteHostId),
bottomPadding = bottomPadding
if (!oneHandUI.value) {
Box(contentAlignment = Alignment.Center) {
val bottomPadding = DEFAULT_PADDING
AppBarTitle(
stringResource(MR.strings.deleted_chats),
hostDevice(rh?.remoteHostId),
bottomPadding = bottomPadding
)
}
}
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val searchShowingSimplexLink = remember { mutableStateOf(false) }
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
val showUnreadAndFavorites = remember { appPrefs.showUnreadAndFavorites.state }.value
val allChats by remember(chatModel.chats.value) {
derivedStateOf { filterContactTypes(chatModel.chats.value, listOf(ContactType.CHAT_DELETED)) }
}
val filteredContactChats = filteredContactChats(
showUnreadAndFavorites = showUnreadAndFavorites,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
searchShowingSimplexLink = searchShowingSimplexLink,
searchText = searchText.value.text,
contactChats = allChats
)
LazyColumnWithScrollBar(
Modifier.fillMaxSize(),
reverseLayout = oneHandUI.value,
) {
item {
if (!oneHandUI.value) {
Divider()
}
ContactsSearchBar(
listState = listState,
searchText = searchText,
searchShowingSimplexLink = searchShowingSimplexLink,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
)
Divider()
Spacer(Modifier.padding(bottom = DEFAULT_PADDING))
}
itemsIndexed(filteredContactChats) { index, chat ->
val nextChatSelected = remember(chat.id, filteredContactChats) {
derivedStateOf {
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
}
}
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false)
}
}
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Text(
generalGetString(MR.strings.no_filtered_contacts),
color = MaterialTheme.colors.secondary,
)
}
}
}
item {
if (!oneHandUI.value) {
Divider()
}
ContactsSearchBar(
listState = listState,
searchText = searchText,
searchShowingSimplexLink = searchShowingSimplexLink,
searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink,
close = close,
)
Divider()
}
item {
if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) {
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Text(
generalGetString(MR.strings.no_filtered_contacts),
color = MaterialTheme.colors.secondary,
)
}
}
}
}
itemsIndexed(filteredContactChats) { index, chat ->
val nextChatSelected = remember(chat.id, filteredContactChats) {
derivedStateOf {
chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value
}
}
ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false)
}
}
}
}
@@ -194,13 +194,13 @@ private fun RetryButton(onClick: () -> Unit) {
@Composable
private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection: MutableState<PendingContactConnection?>) {
SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) {
SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase()) {
LinkTextView(connReqInvitation, true)
}
Spacer(Modifier.height(10.dp))
SectionView(stringResource(MR.strings.or_show_this_qr_code).uppercase(), headerBottomPadding = 5.dp) {
SectionView(stringResource(MR.strings.or_show_this_qr_code).uppercase()) {
SimpleXLinkQRCode(connReqInvitation, onShare = { chatModel.markShowingInvitationUsed() })
}
@@ -242,14 +242,14 @@ fun AddContactLearnMoreButton() {
@Composable
private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState<Boolean>, pastedLink: MutableState<String>, close: () -> Unit) {
SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) {
SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase()) {
PasteLinkView(rhId, pastedLink, showQRCodeScanner, close)
}
if (appPlatform.isAndroid) {
Spacer(Modifier.height(10.dp))
SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase(), headerBottomPadding = 5.dp) {
SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase()) {
QRCodeScanner(showQRCodeScanner) { text ->
withBGApi {
val res = verify(rhId, text, close)
@@ -8,7 +8,9 @@ import SectionSpacer
import SectionView
import TextIconSpaced
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -147,7 +149,7 @@ private fun ConnectDesktop(deviceName: String, remoteCtrls: SnapshotStateList<Re
@Composable
private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
AppBarTitle(stringResource(MR.strings.connecting_to_desktop))
SectionView(stringResource(MR.strings.connecting_to_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.connecting_to_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
CtrlDeviceNameText(session, rc)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
CtrlDeviceVersionText(session)
@@ -197,7 +199,7 @@ private fun SearchingDesktop(deviceName: String, remoteCtrls: SnapshotStateList<
}
}
SectionDividerSpaced()
SectionView(stringResource(MR.strings.found_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.found_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.waiting_for_desktop), fontStyle = FontStyle.Italic)
}
SectionSpacer()
@@ -224,7 +226,7 @@ private fun FoundDesktop(
}
}
SectionDividerSpaced()
SectionView(stringResource(MR.strings.found_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.found_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
CtrlDeviceNameText(session, rc)
CtrlDeviceVersionText(session)
if (!compatible) {
@@ -256,7 +258,7 @@ private fun FoundDesktop(
@Composable
private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessCode: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
AppBarTitle(stringResource(MR.strings.verify_connection))
SectionView(stringResource(MR.strings.connected_to_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.connected_to_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
CtrlDeviceNameText(session, rc)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
CtrlDeviceVersionText(session)
@@ -311,7 +313,7 @@ private fun CtrlDeviceVersionText(session: RemoteCtrlSession) {
@Composable
private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo, close: () -> Unit) {
AppBarTitle(stringResource(MR.strings.connected_to_desktop))
SectionView(stringResource(MR.strings.connected_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.connected_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(rc.deviceViewName)
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
CtrlDeviceVersionText(session)
@@ -53,7 +53,7 @@ expect fun AppearanceView(m: ChatModel)
object AppearanceScope {
@Composable
fun ProfileImageSection() {
SectionView(stringResource(MR.strings.settings_section_title_profile_images).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.settings_section_title_profile_images).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
val image = remember { chatModel.currentUser }.value?.image
Row(Modifier.padding(top = 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
val size = 60
@@ -86,7 +86,7 @@ object AppearanceScope {
@Composable
fun FontScaleSection() {
val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) }
SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(60.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22))
@@ -1,12 +1,15 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionItemView
import SectionItemViewSpaceBetween
import SectionItemViewWithoutMinPadding
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -61,7 +64,7 @@ private fun HiddenProfileLayout(
.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.hide_profile))
SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
SectionView(padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
UserProfileRow(user)
}
SectionSpacer()
@@ -22,6 +22,7 @@ import androidx.compose.ui.text.input.*
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller
@@ -263,7 +264,7 @@ fun SocksProxySettings(
.fillMaxWidth()
) {
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
DefaultConfigurableTextField(
hostUnsaved,
stringResource(MR.strings.host_verb),
@@ -1,10 +1,12 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionCustomFooter
import SectionDividerSpaced
import SectionItemView
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
@@ -32,6 +34,8 @@ import chat.simplex.common.views.onboarding.ReadableText
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.*
import kotlin.math.min
import kotlin.math.roundToInt
enum class LAMode {
SYSTEM,
@@ -370,8 +374,7 @@ fun SimplexLockView(
currentLAMode: SharedPreference<LAMode>,
setPerformLA: (Boolean) -> Unit
) {
val showAuthScreen = remember { chatModel.showAuthScreen }
val performLA = remember { appPrefs.performLA.state }
val performLA = remember { chatModel.performLA }
val laMode = remember { chatModel.controller.appPrefs.laMode.state }
val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay }
val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } }
@@ -379,9 +382,13 @@ fun SimplexLockView(
val selfDestructDisplayName = remember { mutableStateOf(chatModel.controller.appPrefs.selfDestructDisplayName.get() ?: "") }
val selfDestructDisplayNamePref = remember { chatModel.controller.appPrefs.selfDestructDisplayName }
fun resetLAEnabled(onOff: Boolean) {
chatModel.controller.appPrefs.performLA.set(onOff)
chatModel.performLA.value = onOff
}
fun disableUnavailableLA() {
chatModel.controller.appPrefs.performLA.set(false)
chatModel.showAuthScreen.value = false
resetLAEnabled(false)
currentLAMode.set(LAMode.default)
laUnavailableInstructionAlert()
}
@@ -398,8 +405,7 @@ fun SimplexLockView(
} else {
generalGetString(MR.strings.chat_lock)
},
generalGetString(MR.strings.change_lock_mode),
oneTime = true,
generalGetString(MR.strings.change_lock_mode)
) { laResult ->
when (laResult) {
is LAResult.Error -> {
@@ -409,7 +415,7 @@ fun SimplexLockView(
LAResult.Success -> {
when (toLAMode) {
LAMode.SYSTEM -> {
authenticate(generalGetString(MR.strings.auth_enable_simplex_lock), promptSubtitle = "", usingLAMode = toLAMode, oneTime = true) { laResult ->
authenticate(generalGetString(MR.strings.auth_enable_simplex_lock), promptSubtitle = "", usingLAMode = toLAMode) { laResult ->
when (laResult) {
LAResult.Success -> {
currentLAMode.set(toLAMode)
@@ -445,7 +451,7 @@ fun SimplexLockView(
}
fun toggleSelfDestruct(selfDestruct: SharedPreference<Boolean>) {
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_mode), oneTime = true) { laResult ->
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_mode)) { laResult ->
when (laResult) {
is LAResult.Error -> laFailedAlert()
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
@@ -464,7 +470,7 @@ fun SimplexLockView(
}
fun changeLAPassword() {
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode), oneTime = true) { laResult ->
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode)) { laResult ->
when (laResult) {
LAResult.Success -> {
ModalManager.fullscreen.showCustomModal { close ->
@@ -488,7 +494,7 @@ fun SimplexLockView(
}
fun changeSelfDestructPassword() {
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode), oneTime = true) { laResult ->
authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode)) { laResult ->
when (laResult) {
LAResult.Success -> {
ModalManager.fullscreen.showCustomModal { close ->
@@ -519,8 +525,8 @@ fun SimplexLockView(
) {
AppBarTitle(stringResource(MR.strings.chat_lock))
SectionView {
EnableLock(remember { appPrefs.performLA.state }) { performLAToggle ->
showAuthScreen.value = performLAToggle
EnableLock(performLA) { performLAToggle ->
performLA.value = performLAToggle
chatModel.controller.appPrefs.laNoticeShown.set(true)
if (performLAToggle) {
when (currentLAMode.state.value) {
@@ -537,9 +543,7 @@ fun SimplexLockView(
passcodeAlert(generalGetString(MR.strings.passcode_set))
},
cancel = {
chatModel.showAuthScreen.value = false
// Don't drop auth pref in case of state inconsistency (eg, you have set passcode but somehow bypassed toggle and turned it off and then on)
// chatModel.controller.appPrefs.performLA.set(false)
resetLAEnabled(false)
},
close = close
)
@@ -656,7 +660,7 @@ private fun EnableSelfDestruct(
}
@Composable
private fun EnableLock(performLA: State<Boolean>, onCheckedChange: (Boolean) -> Unit) {
private fun EnableLock(performLA: MutableState<Boolean>, onCheckedChange: (Boolean) -> Unit) {
SectionItemView {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
@@ -22,7 +22,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.CreateProfile
@@ -235,7 +234,7 @@ fun ChatLockItem(
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
setPerformLA: (Boolean) -> Unit
) {
val performLA = remember { appPrefs.performLA.state }
val performLA = remember { ChatModel.performLA }
val currentLAMode = remember { ChatModel.controller.appPrefs.laMode }
SettingsActionItemWithContent(
click = showSettingsModal { SimplexLockView(ChatModel, currentLAMode, setPerformLA) },
@@ -506,7 +505,6 @@ private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) ->
authenticate(
title,
desc,
oneTime = true,
completed = { laResult ->
onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable)
}
@@ -8,6 +8,7 @@ import SectionItemViewWithoutMinPadding
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
@@ -269,7 +270,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (
@Composable fun ActionHeader(title: StringResource) {
AppBarTitle(stringResource(title))
SectionView(contentPadding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
SectionView(padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
UserProfileRow(user)
}
SectionSpacer()
@@ -14,6 +14,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
import chat.simplex.common.ui.theme.SimpleXTheme
@@ -26,6 +27,7 @@ import kotlinx.coroutines.*
import java.awt.event.WindowEvent
import java.awt.event.WindowFocusListener
import java.io.File
import kotlin.math.sqrt
import kotlin.system.exitProcess
val simplexWindowState = SimplexWindowState()
@@ -170,7 +172,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
var windowFocused by remember { simplexWindowState.windowFocused }
LaunchedEffect(windowFocused) {
val delay = ChatController.appPrefs.laLockDelay.get()
if (!windowFocused && ChatModel.showAuthScreen.value && delay > 0) {
if (!windowFocused && ChatModel.performLA.value && delay > 0) {
delay(delay * 1000L)
// Trigger auth state check when delay ends (and if it ends)
AppLock.recheckAuthState()
@@ -19,76 +19,12 @@ import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.Closeable
import java.io.File
import java.net.InetSocketAddress
import java.net.Proxy
import kotlin.math.min
data class SemVer(
val major: Int,
val minor: Int,
val patch: Int,
val preRelease: String? = null,
val buildNumber: Int? = null,
): Comparable<SemVer?> {
val isNotStable: Boolean = preRelease != null
override fun compareTo(other: SemVer?): Int {
if (other == null) return 1
return when {
major != other.major -> major.compareTo(other.major)
minor != other.minor -> minor.compareTo(other.minor)
patch != other.patch -> patch.compareTo(other.patch)
preRelease != null && other.preRelease != null -> {
val pr = preRelease.compareTo(other.preRelease, ignoreCase = true)
when {
pr != 0 -> pr
buildNumber != null && other.buildNumber != null -> buildNumber.compareTo(other.buildNumber)
buildNumber != null -> -1
other.buildNumber != null -> 1
else -> 0
}
}
preRelease != null -> -1
other.preRelease != null -> 1
else -> 0
}
}
companion object {
private val regex = Regex("^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([A-Za-z]+)\\.(\\d+))?\$")
fun from(tagName: String): SemVer? {
val trimmed = tagName.trimStart { it == 'v' }
val redacted = when {
trimmed.contains('-') && trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed.substringBefore('-')}.0-${trimmed.substringAfter('-')}"
trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed}.0"
else -> trimmed
}
val group = regex.matchEntire(redacted)?.groups
return if (group != null) {
SemVer(
major = group[1]?.value?.toIntOrNull() ?: return null,
minor = group[2]?.value?.toIntOrNull() ?: return null,
patch = group[3]?.value?.toIntOrNull() ?: return null,
preRelease = group[4]?.value,
buildNumber = group[5]?.value?.toIntOrNull(),
)
} else {
null
}
}
fun fromCurrentVersionName(): SemVer? {
val currentVersionName = if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME
return from(currentVersionName)
}
}
}
@Serializable
data class GitHubRelease(
@@ -98,18 +34,12 @@ data class GitHubRelease(
val htmlUrl: String,
val name: String,
val draft: Boolean,
@SerialName("prerelease")
private val preRelease: Boolean,
val prerelease: Boolean,
val body: String,
@SerialName("published_at")
val publishedAt: String,
val assets: List<GitHubAsset>
) {
@Transient
val semVer: SemVer? = SemVer.from(tagName)
val isConsideredBeta: Boolean = preRelease || semVer == null || semVer.isNotStable
}
)
@Serializable
data class GitHubAsset(
@@ -175,25 +105,25 @@ private fun createUpdateJob() {
fun checkForUpdate() {
Log.d(TAG, "Checking for update")
val currentSemVer = SemVer.fromCurrentVersionName()
if (currentSemVer == null) {
Log.e(TAG, "Current SemVer cannot be parsed")
return
}
val client = setupHttpClient()
try {
val request = Request.Builder().url("https://api.github.com/repos/simplex-chat/simplex-chat/releases").addHeader("User-agent", "curl").build()
client.newCall(request).execute().use { response ->
response.body?.use {
val body = it.string()
val releases = json.decodeFromString<List<GitHubRelease>>(body)
val releases = json.decodeFromString<List<GitHubRelease>>(body).filterNot { it.draft }
val release = when (appPrefs.appUpdateChannel.get()) {
AppUpdatesChannel.STABLE -> releases.firstOrNull { r -> !r.draft && !r.isConsideredBeta && currentSemVer < r.semVer }
AppUpdatesChannel.BETA -> releases.firstOrNull { r -> !r.draft && currentSemVer < r.semVer }
AppUpdatesChannel.STABLE -> releases.firstOrNull { !it.prerelease }
AppUpdatesChannel.BETA -> releases.firstOrNull()
AppUpdatesChannel.DISABLED -> return
} ?: return
val currentVersionName = "v" + (if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME)
val redactedCurrentVersionName = when {
currentVersionName.contains('-') && currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName.substringBefore('-')}.0-${currentVersionName.substringAfter('-')}"
currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName}.0"
else -> currentVersionName
}
if (release == null || release.tagName == appPrefs.appSkippedUpdate.get()) {
if (release.tagName == appPrefs.appSkippedUpdate.get() || release.tagName == currentVersionName || release.tagName == redactedCurrentVersionName) {
Log.d(TAG, "Skipping update because of the same version or skipped version")
return
}
@@ -368,15 +298,13 @@ private suspend fun downloadAsset(asset: GitHubAsset) {
}
}
private fun isRunningFromAppImage(): Boolean = System.getenv("APPIMAGE") != null
private fun isRunningFromFlatpak(): Boolean = System.getenv("container") == "flatpak"
private fun chooseGitHubReleaseAssets(release: GitHubRelease): List<GitHubAsset> {
val res = if (isRunningFromFlatpak()) {
// No need to show download options for Flatpak users
emptyList()
} else if (!isRunningFromAppImage() && Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
} else if (Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) {
// Show all available .deb packages and user will choose the one that works on his system (for Debian derivatives)
release.assets.filter { it.name.lowercase().endsWith(".deb") }
} else {
@@ -7,11 +7,10 @@ actual fun authenticate(
promptSubtitle: String,
selfDestruct: Boolean,
usingLAMode: LAMode,
oneTime: Boolean,
completed: (LAResult) -> Unit
) {
when (usingLAMode) {
LAMode.PASSCODE -> authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, oneTime, completed)
LAMode.PASSCODE -> authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed)
else -> {}
}
}
@@ -25,6 +25,7 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import java.util.Locale
import kotlin.math.roundToInt
@Composable
actual fun AppearanceView(m: ChatModel) {
@@ -43,7 +44,7 @@ fun AppearanceScope.AppearanceLayout(
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.appearance_settings))
SectionView(stringResource(MR.strings.settings_section_title_language), contentPadding = PaddingValues()) {
SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) {
val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") }
LangSelector(state) {
state.value = it
@@ -78,7 +79,7 @@ fun AppearanceScope.AppearanceLayout(
@Composable
fun DensityScaleSection() {
val localDensityScale = remember { mutableStateOf(appPrefs.densityScale.get()) }
SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(60.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22))
@@ -1,63 +0,0 @@
package chat.simplex.app
import chat.simplex.common.views.helpers.SemVer
import kotlin.test.Test
import kotlin.test.assertEquals
// use this command for testing:
// ./gradlew desktopTest
class SemVerTest {
@Test
fun testValidSemVer() {
assertEquals(SemVer.from("1.0.0"), SemVer(1, 0, 0))
assertEquals(SemVer.from("1.0"), SemVer(1, 0, 0))
assertEquals(SemVer.from("v1.0"), SemVer(1, 0, 0))
assertEquals(SemVer.from("v1.0-beta.1"), SemVer(1, 0, 0, "beta", 1))
val r = listOf<Pair<String, SemVer>>(
"0.0.4" to SemVer(0, 0, 4),
"1.2.3" to SemVer(1, 2, 3),
"10.20.30" to SemVer(10, 20, 30),
"1.0.0-alpha.1" to SemVer(1, 0, 0, "alpha", buildNumber = 1),
"1.0.0" to SemVer(1, 0, 0),
"2.0.0" to SemVer(2, 0, 0),
"1.1.7" to SemVer(1, 1, 7),
"2.0.1-alpha.1227" to SemVer(2, 0, 1, "alpha", 1227),
)
r.forEach { (value, correct) ->
assertEquals(SemVer.from(value), correct)
}
}
@Test
fun testComparisonSemVer() {
assert(SemVer(0, 1, 0) == SemVer.from("0.1.0"))
assert(SemVer(1, 1, 0) == SemVer.from("v1.1.0"))
assert(SemVer(0, 1, 0) > SemVer(0, 0, 1))
assert(SemVer(1, 0, 0) > SemVer(0, 100, 100))
assert(SemVer(0, 200, 0) > SemVer(0, 100, 100))
assert(SemVer(0, 1, 0, "beta") > SemVer(0, 1, 0, "alpha"))
assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "alpha"))
assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta"))
assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta.0"))
assert(SemVer(0, 1, 0, "beta", 1) > SemVer(0, 1, 0, "beta", 0))
assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 10))
assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 9))
assert(SemVer(0, 1, 0, "beta.1") > SemVer(0, 1, 0, "alpha.2"))
assert(SemVer(1, 1, 0, "beta.1") > SemVer(0, 1, 0, "beta.1"))
assert(SemVer(1, 0, 0) > SemVer(1, 0, 0, "beta.1"))
assert(SemVer(1, 0, 0) > null)
assert(SemVer.from("v6.0.0")!! > SemVer.from("v6.0.0-beta.3"))
assert(SemVer.from("v6.0.0-beta.3")!! > SemVer.from("v6.0.0-beta.2"))
assert(SemVer.from("0.1.0") == SemVer.from("0.1.0"))
assert(SemVer.from("0.1.1")!! > SemVer.from("0.1.0"))
assert(SemVer.from("0.2.1")!! > SemVer.from("0.1.1"))
assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1"))
assert(SemVer.from("0.1.1-beta.0")!! > SemVer.from("0.1.0-beta.0"))
assert(SemVer.from("0.1.1-beta.0")!! == SemVer.from("0.1.1-beta.0"))
assert(SemVer.from("0.1.1-beta.1")!! > SemVer.from("0.1.1-beta.0"))
assert(SemVer.from("10.0.0-beta.12")!! > SemVer.from("1.1.1"))
assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.9"))
assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.120"))
assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1"))
}
}
+4 -4
View File
@@ -26,11 +26,11 @@ android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=6.0.4
android.version_code=237
android.version_name=6.0.2
android.version_code=234
desktop.version_name=6.0.4
desktop.version_code=65
desktop.version_name=6.0.2
desktop.version_code=63
kotlin.version=1.9.23
gradle.plugin.version=8.2.0
+1 -1
View File
@@ -46,7 +46,7 @@ mySquaringBot _user cc = do
CRContactConnected _ contact _ -> do
contactConnected contact
sendMessage cc contact welcomeMessage
CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) -> do
CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do
let msg = T.unpack $ ciContentToText mc
number_ = readMaybe msg :: Maybe Integer
sendMessage cc contact $ case number_ of
@@ -40,7 +40,7 @@ broadcastBot BroadcastBotOpts {publishers, welcomeMessage, prohibitedMessage} _u
CRContactConnected _ ct _ -> do
contactConnected ct
sendMessage cc ct welcomeMessage
CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc})
CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc}) : _}
| publisher `elem` publishers ->
if allowContent mc
then do
@@ -73,7 +73,7 @@ crDirectoryEvent = \case
CRGroupDeleted {groupInfo} -> Just $ DEGroupDeleted groupInfo
CRChatItemUpdated {chatItem = AChatItem _ SMDRcv (DirectChat ct) _} -> Just $ DEItemEditIgnored ct
CRChatItemsDeleted {chatItemDeletions = ((ChatItemDeletion (AChatItem _ SMDRcv (DirectChat ct) _) _) : _), byUser = False} -> Just $ DEItemDeleteIgnored ct
CRNewChatItem {chatItem = AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}} ->
CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}) : _} ->
Just $ case (mc, itemLive) of
(MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.dropWhileEnd isSpace t
_ -> DEUnsupportedMessage ct ciId
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: d559a66145cf7b4cd367c09974ed1ce8393940b2
tag: 1d22608f860636f21a5557f1b3fab4a7da09c5cc
source-repository-package
type: git
Generated
+29 -82
View File
@@ -156,11 +156,11 @@
"ghc98X": {
"flake": false,
"locked": {
"lastModified": 1715066704,
"narHash": "sha256-F0EVR8x/fcpj1st+hz96Wdsz5uwVIOziGKAwRxLOYJw=",
"lastModified": 1696643148,
"narHash": "sha256-E02DfgISH7EvvNAu0BHiPvl1E5FGMDi0pWdNZtIBC9I=",
"ref": "ghc-9.8",
"rev": "78a253543d466ac511a1664a3e6aff032ca684d5",
"revCount": 61757,
"rev": "443e870d977b1ab6fc05f47a9a17bc49296adbd6",
"revCount": 61642,
"submodules": true,
"type": "git",
"url": "https://gitlab.haskell.org/ghc/ghc"
@@ -175,11 +175,11 @@
"ghc99": {
"flake": false,
"locked": {
"lastModified": 1726585445,
"narHash": "sha256-IdwQBex4boY6s0Plj5+ixf36rfYSUyMdTWrztKvZH30=",
"lastModified": 1697054644,
"narHash": "sha256-kKarOuXUaAH3QWv7ASx+gGFMHaHKe0pK5Zu37ky2AL4=",
"ref": "refs/heads/master",
"rev": "7fd9e5e29ab54eb406880077463e8552e2ddd39a",
"revCount": 67238,
"rev": "f383a242c76f90bcca8a4d7ee001dcb49c172a9a",
"revCount": 62040,
"submodules": true,
"type": "git",
"url": "https://gitlab.haskell.org/ghc/ghc"
@@ -225,8 +225,6 @@
"hls-2.2": "hls-2.2",
"hls-2.3": "hls-2.3",
"hls-2.4": "hls-2.4",
"hls-2.5": "hls-2.5",
"hls-2.6": "hls-2.6",
"hpc-coveralls": "hpc-coveralls",
"hydra": "hydra",
"iserv-proxy": "iserv-proxy",
@@ -240,17 +238,16 @@
"nixpkgs-2205": "nixpkgs-2205",
"nixpkgs-2211": "nixpkgs-2211",
"nixpkgs-2305": "nixpkgs-2305",
"nixpkgs-2311": "nixpkgs-2311",
"nixpkgs-unstable": "nixpkgs-unstable",
"old-ghc-nix": "old-ghc-nix",
"stackage": "stackage"
},
"locked": {
"lastModified": 1705833500,
"narHash": "sha256-rUIr6JNbCedt1g4gVYVvE9t0oFU6FUspCA0DS5cA8Bg=",
"lastModified": 1701163700,
"narHash": "sha256-sOrewUS3LnzV09nGr7+3R6Q6zsgU4smJc61QsHq+4DE=",
"owner": "input-output-hk",
"repo": "haskell.nix",
"rev": "d0c35e75cbbc6858770af42ac32b0b85495fbd71",
"rev": "2808bfe3e62e9eb4ee8974cd623a00e1611f302b",
"type": "github"
},
"original": {
@@ -331,50 +328,16 @@
"hls-2.4": {
"flake": false,
"locked": {
"lastModified": 1699862708,
"narHash": "sha256-YHXSkdz53zd0fYGIYOgLt6HrA0eaRJi9mXVqDgmvrjk=",
"lastModified": 1696939266,
"narHash": "sha256-VOMf5+kyOeOmfXTHlv4LNFJuDGa7G3pDnOxtzYR40IU=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "54507ef7e85fa8e9d0eb9a669832a3287ffccd57",
"rev": "362fdd1293efb4b82410b676ab1273479f6d17ee",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.4.0.1",
"repo": "haskell-language-server",
"type": "github"
}
},
"hls-2.5": {
"flake": false,
"locked": {
"lastModified": 1701080174,
"narHash": "sha256-fyiR9TaHGJIIR0UmcCb73Xv9TJq3ht2ioxQ2mT7kVdc=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "27f8c3d3892e38edaef5bea3870161815c4d014c",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.5.0.0",
"repo": "haskell-language-server",
"type": "github"
}
},
"hls-2.6": {
"flake": false,
"locked": {
"lastModified": 1705325287,
"narHash": "sha256-+P87oLdlPyMw8Mgoul7HMWdEvWP/fNlo8jyNtwME8E8=",
"owner": "haskell",
"repo": "haskell-language-server",
"rev": "6e0b342fa0327e628610f2711f8c3e4eaaa08b1e",
"type": "github"
},
"original": {
"owner": "haskell",
"ref": "2.6.0.0",
"ref": "2.4.0.0",
"repo": "haskell-language-server",
"type": "github"
}
@@ -421,11 +384,11 @@
"iserv-proxy": {
"flake": false,
"locked": {
"lastModified": 1707968597,
"narHash": "sha256-C53NqToxl+n9s1pQ0iLtiH6P5vX3rM+NW/mFt4Ykpsk=",
"lastModified": 1691634696,
"narHash": "sha256-MZH2NznKC/gbgBu8NgIibtSUZeJ00HTLJ0PlWKCBHb0=",
"ref": "hkm/remote-iserv",
"rev": "1b7f8aeb37bbc7c00f04e44d9379aa15a4409e8b",
"revCount": 18,
"rev": "43a979272d9addc29fbffc2e8542c5d96e993d73",
"revCount": 14,
"type": "git",
"url": "https://gitlab.haskell.org/hamishmack/iserv-proxy.git"
},
@@ -589,11 +552,11 @@
},
"nixpkgs-2305": {
"locked": {
"lastModified": 1705033721,
"narHash": "sha256-K5eJHmL1/kev6WuqyqqbS1cdNnSidIZ3jeqJ7GbrYnQ=",
"lastModified": 1695416179,
"narHash": "sha256-610o1+pwbSu+QuF3GE0NU5xQdTHM3t9wyYhB9l94Cd8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a1982c92d8980a0114372973cbdfe0a307f1bdea",
"rev": "715d72e967ec1dd5ecc71290ee072bcaf5181ed6",
"type": "github"
},
"original": {
@@ -603,22 +566,6 @@
"type": "github"
}
},
"nixpkgs-2311": {
"locked": {
"lastModified": 1719957072,
"narHash": "sha256-gvFhEf5nszouwLAkT9nWsDzocUTqLWHuL++dvNjMp9I=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7144d6241f02d171d25fba3edeaf15e0f2592105",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-23.11-darwin",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
@@ -655,17 +602,17 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1694822471,
"narHash": "sha256-6fSDCj++lZVMZlyqOe9SIOL8tYSBz1bI8acwovRwoX8=",
"lastModified": 1695318763,
"narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "47585496bcb13fb72e4a90daeea2f434e2501998",
"rev": "e12483116b3b51a185a33a272bf351e357ba9a99",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"rev": "47585496bcb13fb72e4a90daeea2f434e2501998",
"type": "github"
}
},
@@ -717,11 +664,11 @@
"stackage": {
"flake": false,
"locked": {
"lastModified": 1726532152,
"narHash": "sha256-LRXbVY3M2S8uQWdwd2zZrsnVPEvt2GxaHGoy8EFFdJA=",
"lastModified": 1699834215,
"narHash": "sha256-g/JKy0BCvJaxPuYDl3QVc4OY8cFEomgG+hW/eEV470M=",
"owner": "input-output-hk",
"repo": "stackage.nix",
"rev": "c77b3530cebad603812cb111c6f64968c2d2337d",
"rev": "47aacd04abcce6bad57f43cbbbd133538380248e",
"type": "github"
},
"original": {
+1 -2
View File
@@ -1,5 +1,5 @@
name: simplex-chat
version: 6.0.4.0
version: 6.1.0.0
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
@@ -48,7 +48,6 @@ dependencies:
- tls >= 1.9.0 && < 1.10
- unliftio == 0.2.*
- unliftio-core == 0.2.*
- uuid == 1.3.*
- zip == 2.0.*
flags:
@@ -38,38 +38,6 @@
</description>
<releases>
<release version="6.0.3" date="2024-08-24">
<url type="details">https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html</url>
<description>
<p>New in v6.0.1-3:</p>
<ul>
<li>reduce app memory usage and start time.</li>
<li>faster sending files to groups.</li>
<li>fix rare delivery bug.</li>
</ul>
<p>New in v6.0:</p>
<p>New chat experience:</p>
<ul>
<li>connect to your friends faster.</li>
<li>archive contacts to chat later.</li>
<li>delete up to 20 messages at once.</li>
<li>increase font size.</li>
</ul>
<p>New media options:</p>
<ul>
<li>play from the chat list.</li>
<li>blur for better privacy.</li>
</ul>
<p>Private routing:</p>
<ul>
<li>it protects your IP address and connections and is now enabled by default.</li>
</ul>
<p>Connection and servers information:</p>
<ul>
<li>to control your network status and usage.</li>
</ul>
</description>
</release>
<release version="6.0.0" date="2024-08-12">
<url type="details">https://github.com/simplex-chat/simplex-chat/releases/tag/v6.0.0</url>
<description>
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."d559a66145cf7b4cd367c09974ed1ce8393940b2" = "1jav7jmriims6vlkxg8gmal03f9mbgrwc8v6g0rp95ivkx8gfjyw";
"https://github.com/simplex-chat/simplexmq.git"."1d22608f860636f21a5557f1b3fab4a7da09c5cc" = "16kmc05avzdyd6kpj83nyqkyjks5kim5j351397f6p3yvm7iydwz";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+1 -9
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 6.0.4.0
version: 6.1.0.0
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
@@ -146,7 +146,6 @@ library
Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
Simplex.Chat.Migrations.M20240528_quota_err_counter
Simplex.Chat.Migrations.M20240827_calls_uuid
Simplex.Chat.Mobile
Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared
@@ -230,7 +229,6 @@ library
, tls >=1.9.0 && <1.10
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, uuid ==1.3.*
, zip ==2.0.*
default-language: Haskell2010
if flag(swift)
@@ -294,7 +292,6 @@ executable simplex-bot
, tls >=1.9.0 && <1.10
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, uuid ==1.3.*
, zip ==2.0.*
default-language: Haskell2010
if flag(swift)
@@ -358,7 +355,6 @@ executable simplex-bot-advanced
, tls >=1.9.0 && <1.10
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, uuid ==1.3.*
, zip ==2.0.*
default-language: Haskell2010
if flag(swift)
@@ -425,7 +421,6 @@ executable simplex-broadcast-bot
, tls >=1.9.0 && <1.10
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, uuid ==1.3.*
, zip ==2.0.*
default-language: Haskell2010
if flag(swift)
@@ -490,7 +485,6 @@ executable simplex-chat
, tls >=1.9.0 && <1.10
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, uuid ==1.3.*
, websockets ==0.12.*
, zip ==2.0.*
default-language: Haskell2010
@@ -561,7 +555,6 @@ executable simplex-directory-service
, tls >=1.9.0 && <1.10
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, uuid ==1.3.*
, zip ==2.0.*
default-language: Haskell2010
if flag(swift)
@@ -662,7 +655,6 @@ test-suite simplex-chat-test
, tls >=1.9.0 && <1.10
, unliftio ==0.2.*
, unliftio-core ==0.2.*
, uuid ==1.3.*
, zip ==2.0.*
default-language: Haskell2010
if flag(swift)
+478 -294
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -11,6 +11,7 @@ import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Monad
import qualified Data.ByteString.Char8 as B
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Text as T
import Simplex.Chat.Controller
import Simplex.Chat.Core
@@ -31,7 +32,7 @@ chatBotRepl welcome answer _user cc = do
CRContactConnected _ contact _ -> do
contactConnected contact
void $ sendMessage cc contact welcome
CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) -> do
CRNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do
let msg = T.unpack $ ciContentToText mc
void $ sendMessage cc contact =<< answer contact msg
_ -> pure ()
@@ -68,8 +69,8 @@ sendComposedMessage cc = sendComposedMessage' cc . contactId'
sendComposedMessage' :: ChatController -> ContactId -> Maybe ChatItemId -> MsgContent -> IO ()
sendComposedMessage' cc ctId quotedItemId msgContent = do
let cm = ComposedMessage {fileSource = Nothing, quotedItemId, msgContent}
sendChatCmd cc (APISendMessage (ChatRef CTDirect ctId) False Nothing cm) >>= \case
CRNewChatItem {} -> printLog cc CLLInfo $ "sent message to contact ID " <> show ctId
sendChatCmd cc (APISendMessages (ChatRef CTDirect ctId) False Nothing (cm :| [])) >>= \case
CRNewChatItems {} -> printLog cc CLLInfo $ "sent message to contact ID " <> show ctId
r -> putStrLn $ "unexpected send message response: " <> show r
deleteMessage :: ChatController -> Contact -> ChatItemId -> IO ()
-2
View File
@@ -29,7 +29,6 @@ import Simplex.Messaging.Util (decodeJSON, encodeJSON)
data Call = Call
{ contactId :: ContactId,
callId :: CallId,
callUUID :: Text,
chatItemId :: Int64,
callState :: CallState,
callTs :: UTCTime
@@ -112,7 +111,6 @@ data RcvCallInvitation = RcvCallInvitation
contact :: Contact,
callType :: CallType,
sharedKey :: Maybe C.Key,
callUUID :: Text,
callTs :: UTCTime
}
deriving (Show)
+4 -5
View File
@@ -292,13 +292,13 @@ data ChatCommand
| APIGetChat ChatRef ChatPagination (Maybe String)
| APIGetChatItems ChatPagination (Maybe String)
| APIGetChatItemInfo ChatRef ChatItemId
| APISendMessage {chatRef :: ChatRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessage :: ComposedMessage}
| APICreateChatItem {noteFolderId :: NoteFolderId, composedMessage :: ComposedMessage}
| APISendMessages {chatRef :: ChatRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessages :: NonEmpty ComposedMessage}
| APICreateChatItems {noteFolderId :: NoteFolderId, composedMessages :: NonEmpty ComposedMessage}
| APIUpdateChatItem {chatRef :: ChatRef, chatItemId :: ChatItemId, liveMessage :: Bool, msgContent :: MsgContent}
| APIDeleteChatItem ChatRef (NonEmpty ChatItemId) CIDeleteMode
| APIDeleteMemberChatItem GroupId (NonEmpty ChatItemId)
| APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction}
| APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId, ttl :: Maybe Int}
| APIForwardChatItems {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int}
| APIUserRead UserId
| UserRead
| APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId))
@@ -597,7 +597,7 @@ data ChatResponse
| CRContactCode {user :: User, contact :: Contact, connectionCode :: Text}
| CRGroupMemberCode {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text}
| CRConnectionVerified {user :: User, verified :: Bool, expectedCode :: Text}
| CRNewChatItem {user :: User, chatItem :: AChatItem}
| CRNewChatItems {user :: User, chatItems :: [AChatItem]}
| CRChatItemStatusUpdated {user :: User, chatItem :: AChatItem}
| CRChatItemUpdated {user :: User, chatItem :: AChatItem}
| CRChatItemNotChanged {user :: User, chatItem :: AChatItem}
@@ -1178,7 +1178,6 @@ data ChatErrorType
| CEInlineFileProhibited {fileId :: FileTransferId}
| CEInvalidQuote
| CEInvalidForward
| CEForwardNoFile
| CEInvalidChatItemUpdate
| CEInvalidChatItemDelete
| CEHasCurrentCall
+6 -4
View File
@@ -17,16 +17,18 @@ import Simplex.Chat.Messages
data MsgBatch = MsgBatch ByteString [SndMessage]
-- | Batches [SndMessage] into batches of ByteStrings in form of JSON arrays.
-- | Batches SndMessages in [Either ChatError SndMessage] into batches of ByteStrings in form of JSON arrays.
-- Preserves original errors in the list.
-- Does not check if the resulting batch is a valid JSON.
-- If a single element is passed, it is returned as is (a JSON string).
-- If an element exceeds maxLen, it is returned as ChatError.
batchMessages :: Int -> [SndMessage] -> [Either ChatError MsgBatch]
batchMessages :: Int -> [Either ChatError SndMessage] -> [Either ChatError MsgBatch]
batchMessages maxLen = addBatch . foldr addToBatch ([], [], 0, 0)
where
msgBatch batch = Right (MsgBatch (encodeMessages batch) batch)
addToBatch :: SndMessage -> ([Either ChatError MsgBatch], [SndMessage], Int, Int) -> ([Either ChatError MsgBatch], [SndMessage], Int, Int)
addToBatch msg@SndMessage {msgBody} acc@(batches, batch, len, n)
addToBatch :: Either ChatError SndMessage -> ([Either ChatError MsgBatch], [SndMessage], Int, Int) -> ([Either ChatError MsgBatch], [SndMessage], Int, Int)
addToBatch (Left err) acc = (Left err : addBatch acc, [], 0, 0) -- step over original error
addToBatch (Right msg@SndMessage {msgBody}) acc@(batches, batch, len, n)
| batchLen <= maxLen = (batches, msg : batch, len', n + 1)
| msgLen <= maxLen = (addBatch acc, [msg], msgLen, 1)
| otherwise = (errLarge msg : addBatch acc, [], 0, 0)
@@ -1,18 +0,0 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240827_calls_uuid where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240827_calls_uuid :: Query
m20240827_calls_uuid =
[sql|
ALTER TABLE calls ADD COLUMN call_uuid TEXT NOT NULL DEFAULT "";
|]
down_m20240827_calls_uuid :: Query
down_m20240827_calls_uuid =
[sql|
ALTER TABLE calls DROP COLUMN call_uuid;
|]
@@ -415,8 +415,6 @@ CREATE TABLE calls(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
,
call_uuid TEXT NOT NULL DEFAULT ""
);
CREATE TABLE commands(
command_id INTEGER PRIMARY KEY AUTOINCREMENT, -- used as ACorrId
+2 -2
View File
@@ -72,11 +72,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis
-- when acting as host
minRemoteCtrlVersion :: AppVersion
minRemoteCtrlVersion = AppVersion [6, 0, 0, 4]
minRemoteCtrlVersion = AppVersion [6, 1, 0, 0]
-- when acting as controller
minRemoteHostVersion :: AppVersion
minRemoteHostVersion = AppVersion [6, 0, 0, 4]
minRemoteHostVersion = AppVersion [6, 1, 0, 0]
currentAppVersion :: AppVersion
currentAppVersion = AppVersion SC.version
+5 -5
View File
@@ -966,20 +966,20 @@ lookupFileTransferRedirectMeta db User {userId} fileId = do
redirects <- DB.query db "SELECT file_id FROM files WHERE user_id = ? AND redirect_file_id = ?" (userId, fileId)
rights <$> mapM (runExceptT . getFileTransferMeta_ db userId . fromOnly) redirects
createLocalFile :: ToField (CIFileStatus d) => CIFileStatus d -> DB.Connection -> User -> NoteFolder -> ChatItemId -> UTCTime -> CryptoFile -> Integer -> Integer -> IO Int64
createLocalFile fileStatus db User {userId} NoteFolder {noteFolderId} chatItemId itemTs CryptoFile {filePath, cryptoArgs} fileSize fileChunkSize = do
createLocalFile :: ToField (CIFileStatus d) => CIFileStatus d -> DB.Connection -> User -> NoteFolder -> UTCTime -> CryptoFile -> Integer -> Integer -> IO Int64
createLocalFile fileStatus db User {userId} NoteFolder {noteFolderId} itemTs CryptoFile {filePath, cryptoArgs} fileSize fileChunkSize = do
DB.execute
db
[sql|
INSERT INTO files
( user_id, note_folder_id, chat_item_id,
( user_id, note_folder_id,
file_name, file_path, file_size,
file_crypto_key, file_crypto_nonce,
chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at
)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (userId, noteFolderId, chatItemId)
( (userId, noteFolderId)
:. (takeFileName filePath, filePath, fileSize)
:. maybe (Nothing, Nothing) (\(CFArgs key nonce) -> (Just key, Just nonce)) cryptoArgs
:. (fileChunkSize, Nothing :: Maybe InlineFileMode, fileStatus, FPLocal, itemTs, itemTs)
+1 -3
View File
@@ -110,7 +110,6 @@ import Simplex.Chat.Migrations.M20240501_chat_deleted
import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
import Simplex.Chat.Migrations.M20240528_quota_err_counter
import Simplex.Chat.Migrations.M20240827_calls_uuid
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -220,8 +219,7 @@ schemaMigrations =
("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted),
("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy),
("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays),
("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter),
("20240827_calls_uuid", m20240827_calls_uuid, Just down_m20240827_calls_uuid)
("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter)
]
-- | The list of migrations in ascending order by date
+7 -7
View File
@@ -549,17 +549,17 @@ overwriteProtocolServers db User {userId} servers =
protocol = decodeLatin1 $ strEncode $ protocolTypeI @p
createCall :: DB.Connection -> User -> Call -> UTCTime -> IO ()
createCall db user@User {userId} Call {contactId, callId, callUUID, chatItemId, callState} callTs = do
createCall db user@User {userId} Call {contactId, callId, chatItemId, callState} callTs = do
currentTs <- getCurrentTime
deleteCalls db user contactId
DB.execute
db
[sql|
INSERT INTO calls
(contact_id, shared_call_id, call_uuid, chat_item_id, call_state, call_ts, user_id, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?)
(contact_id, shared_call_id, chat_item_id, call_state, call_ts, user_id, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?)
|]
(contactId, callId, callUUID, chatItemId, callState, callTs, userId, currentTs, currentTs)
(contactId, callId, chatItemId, callState, callTs, userId, currentTs, currentTs)
deleteCalls :: DB.Connection -> User -> ContactId -> IO ()
deleteCalls db User {userId} contactId = do
@@ -572,13 +572,13 @@ getCalls db =
db
[sql|
SELECT
contact_id, shared_call_id, call_uuid, chat_item_id, call_state, call_ts
contact_id, shared_call_id, chat_item_id, call_state, call_ts
FROM calls
ORDER BY call_ts ASC
|]
where
toCall :: (ContactId, CallId, Text, ChatItemId, CallState, UTCTime) -> Call
toCall (contactId, callId, callUUID, chatItemId, callState, callTs) = Call {contactId, callId, callUUID, chatItemId, callState, callTs}
toCall :: (ContactId, CallId, ChatItemId, CallState, UTCTime) -> Call
toCall (contactId, callId, chatItemId, callState, callTs) = Call {contactId, callId, chatItemId, callState, callTs}
createCommand :: DB.Connection -> User -> Maybe Int64 -> CommandFunction -> IO CommandId
createCommand db User {userId} connId commandFunction = do
+2 -2
View File
@@ -69,7 +69,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
Nothing -> setActive ct ""
Just rhId -> updateRemoteUser ct u rhId
CRChatItems u chatName_ _ -> whenCurrUser cc u $ mapM_ (setActive ct . chatActiveTo) chatName_
CRNewChatItem u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo
CRNewChatItems u ((AChatItem _ SMDSnd cInfo _) : _) -> whenCurrUser cc u $ setActiveChat ct cInfo
CRChatItemUpdated u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo
CRChatItemsDeleted u ((ChatItemDeletion (AChatItem _ _ cInfo _) _) : _) _ _ -> whenCurrUser cc u $ setActiveChat ct cInfo
CRContactDeleted u c -> whenCurrUser cc u $ unsetActiveContact ct c
@@ -93,7 +93,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
Right SendMessageBroadcast {} -> True
_ -> False
startLiveMessage :: Either a ChatCommand -> ChatResponse -> IO ()
startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItem _ (AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}})) = do
startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItems {chatItems = [AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}}]}) = do
whenM (isNothing <$> readTVarIO liveMessageState) $ do
let s = T.unpack msg
int = case cType of SCTGroup -> 5000000; _ -> 3000000 :: Int
+1 -1
View File
@@ -44,7 +44,7 @@ simplexChatCLI' cfg opts@ChatOpts {chatCmd, chatCmdLog, chatCmdDelay, chatServer
when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do
(_, _, r') <- atomically . readTBQueue $ outputQ cc
case r' of
CRNewChatItem {} -> printResponse r'
CRNewChatItems {} -> printResponse r'
_ -> when (chatCmdLog == CCLAll) $ printResponse r'
sendChatCmdStr cc chatCmd >>= printResponse
threadDelay $ chatCmdDelay * 1000000
+3 -2
View File
@@ -147,7 +147,7 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} Cha
forever $ do
(_, outputRH, r) <- atomically $ readTBQueue outputQ
case r of
CRNewChatItem u ci -> when markRead $ markChatItemRead u ci
CRNewChatItems u (ci : _) -> when markRead $ markChatItemRead u ci -- At the moment of writing received items are created one at a time
CRChatItemUpdated u ci -> when markRead $ markChatItemRead u ci
CRRemoteHostConnected {remoteHost = RemoteHostInfo {remoteHostId}} -> getRemoteUser remoteHostId
CRRemoteHostStopped {remoteHostId_} -> mapM_ removeRemoteUser remoteHostId_
@@ -175,7 +175,8 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} Cha
responseNotification :: ChatTerminal -> ChatController -> ChatResponse -> IO ()
responseNotification t@ChatTerminal {sendNotification} cc = \case
CRNewChatItem u (AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent mc, formattedText}) ->
-- At the moment of writing received items are created one at a time
CRNewChatItems u ((AChatItem _ SMDRcv cInfo ci@ChatItem {chatDir, content = CIRcvMsgContent mc, formattedText}) : _) ->
when (chatDirNtf u cInfo chatDir $ isMention ci) $ do
whenCurrUser cc u $ setActiveChat t cInfo
case (cInfo, chatDir) of
+4 -2
View File
@@ -120,7 +120,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView
CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView
CRNewChatItem u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item
CRNewChatItems u chatItems ->
concatMap
(\(AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewChatItem chat item False ts tz <> viewItemReactions item)
chatItems
CRChatItems u _ chatItems -> ttyUser u $ concatMap (\(AChatItem _ _ chat item) -> viewChatItem chat item True ts tz <> viewItemReactions item) chatItems
CRChatItemInfo u ci ciInfo -> ttyUser u $ viewChatItemInfo ci ciInfo tz
CRChatItemId u itemId -> ttyUser u [plain $ maybe "no item" show itemId]
@@ -2025,7 +2028,6 @@ viewChatError isCmd logLevel testView = \case
CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."]
CEInvalidQuote -> ["cannot reply to this message"]
CEInvalidForward -> ["cannot forward this message"]
CEForwardNoFile -> ["cannot forward this message, file not found"]
CEInvalidChatItemUpdate -> ["cannot update this item"]
CEInvalidChatItemDelete -> ["cannot delete this item"]
CEHasCurrentCall -> ["call already in progress"]
-1
View File
@@ -442,7 +442,6 @@ smpServerCfg =
logStatsStartTime = 0,
serverStatsLogFile = "tests/smp-server-stats.daily.log",
serverStatsBackupFile = Nothing,
pendingENDInterval = 500000,
smpServerVRange = supportedServerSMPRelayVRange,
transportConfig = defaultTransportServerConfig {alpn = Just supportedSMPHandshakes},
smpHandshakeTimeout = 1000000,
+15 -2
View File
@@ -52,6 +52,7 @@ chatDirectTests = do
it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact
it "should send multiline message" testMultilineMessage
it "send large message" testLargeMessage
it "send multiple messages api" testSendMulti
describe "duplicate contacts" $ do
it "duplicate contacts are separate (contacts don't merge)" testDuplicateContactsSeparate
it "new contact is separate with multiple duplicate contacts (contacts don't merge)" testDuplicateContactsMultipleSeparate
@@ -839,6 +840,18 @@ testLargeMessage =
bob <## "contact alice changed to alice2"
bob <## "use @alice2 <message> to send messages"
testSendMulti :: HasCallStack => FilePath -> IO ()
testSendMulti =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
alice ##> "/_send @2 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}]"
alice <# "@bob test 1"
alice <# "@bob test 2"
bob <# "alice> test 1"
bob <# "alice> test 2"
testGetSetSMPServers :: HasCallStack => FilePath -> IO ()
testGetSetSMPServers =
testChat2 aliceProfile bobProfile $
@@ -2162,7 +2175,7 @@ testSetChatItemTTL =
-- chat item with file
alice #$> ("/_files_folder ./tests/tmp/app_files", id, "ok")
copyFile "./tests/fixtures/test.jpg" "./tests/tmp/app_files/test.jpg"
alice ##> "/_send @2 json {\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}"
alice ##> "/_send @2 json [{\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]"
alice <# "/f @bob test.jpg"
alice <## "use /fc 1 to cancel sending"
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
@@ -2410,7 +2423,7 @@ setupDesynchronizedRatchet tmp alice = do
(bob </)
bob ##> "/tail @alice 1"
bob <# "alice> decryption error, possibly due to the device change (header, 3 messages)"
bob ##> "@alice 1"
bob `send` "@alice 1"
bob <## "error: command is prohibited, sendMessagesB: send prohibited"
(alice </)
where
+12 -12
View File
@@ -64,7 +64,7 @@ runTestMessageWithFile :: HasCallStack => FilePath -> IO ()
runTestMessageWithFile = testChat2 aliceProfile bobProfile $ \alice bob -> withXFTPServer $ do
connectUsers alice bob
alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}"
alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}]"
alice <# "@bob hi, sending a file"
alice <# "/f @bob ./tests/fixtures/test.jpg"
alice <## "use /fc 1 to cancel sending"
@@ -91,7 +91,7 @@ testSendImage =
testChat2 aliceProfile bobProfile $
\alice bob -> withXFTPServer $ do
connectUsers alice bob
alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}"
alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]"
alice <# "/f @bob ./tests/fixtures/test.jpg"
alice <## "use /fc 1 to cancel sending"
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
@@ -122,7 +122,7 @@ testSenderMarkItemDeleted =
testChat2 aliceProfile bobProfile $
\alice bob -> withXFTPServer $ do
connectUsers alice bob
alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test_1MB.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}"
alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test_1MB.pdf\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}]"
alice <# "@bob hi, sending a file"
alice <# "/f @bob ./tests/fixtures/test_1MB.pdf"
alice <## "use /fc 1 to cancel sending"
@@ -147,7 +147,7 @@ testFilesFoldersSendImage =
connectUsers alice bob
alice #$> ("/_files_folder ./tests/fixtures", id, "ok")
bob #$> ("/_files_folder ./tests/tmp/app_files", id, "ok")
alice ##> "/_send @2 json {\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}"
alice ##> "/_send @2 json [{\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]"
alice <# "/f @bob test.jpg"
alice <## "use /fc 1 to cancel sending"
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
@@ -180,7 +180,7 @@ testFilesFoldersImageSndDelete =
alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok")
copyFile "./tests/fixtures/test_1MB.pdf" "./tests/tmp/alice_app_files/test_1MB.pdf"
bob #$> ("/_files_folder ./tests/tmp/bob_app_files", id, "ok")
alice ##> "/_send @2 json {\"filePath\": \"test_1MB.pdf\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}"
alice ##> "/_send @2 json [{\"filePath\": \"test_1MB.pdf\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]"
alice <# "/f @bob test_1MB.pdf"
alice <## "use /fc 1 to cancel sending"
bob <# "alice> sends file test_1MB.pdf (1017.7 KiB / 1042157 bytes)"
@@ -212,7 +212,7 @@ testFilesFoldersImageRcvDelete =
connectUsers alice bob
alice #$> ("/_files_folder ./tests/fixtures", id, "ok")
bob #$> ("/_files_folder ./tests/tmp/app_files", id, "ok")
alice ##> "/_send @2 json {\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}"
alice ##> "/_send @2 json [{\"filePath\": \"test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]"
alice <# "/f @bob test.jpg"
alice <## "use /fc 1 to cancel sending"
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
@@ -239,7 +239,7 @@ testSendImageWithTextAndQuote =
connectUsers alice bob
bob #> "@alice hi alice"
alice <# "bob> hi alice"
alice ##> ("/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 1 <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}")
alice ##> ("/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 1 <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]")
alice <# "@bob > hi alice"
alice <## " hey bob"
alice <# "/f @bob ./tests/fixtures/test.jpg"
@@ -265,7 +265,7 @@ testSendImageWithTextAndQuote =
bob @@@ [("@alice", "hey bob")]
-- quoting (file + text) with file uses quoted text
bob ##> ("/_send @2 json {\"filePath\": \"./tests/fixtures/test.pdf\", \"quotedItemId\": " <> itemId 2 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"file\"}}")
bob ##> ("/_send @2 json [{\"filePath\": \"./tests/fixtures/test.pdf\", \"quotedItemId\": " <> itemId 2 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"file\"}}]")
bob <# "@alice > hey bob"
bob <## " test.pdf"
bob <# "/f @alice ./tests/fixtures/test.pdf"
@@ -287,7 +287,7 @@ testSendImageWithTextAndQuote =
B.readFile "./tests/tmp/test.pdf" `shouldReturn` txtSrc
-- quoting (file without text) with file uses file name
alice ##> ("/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 3 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}")
alice ##> ("/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> itemId 3 <> ", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]")
alice <# "@bob > test.pdf"
alice <## " test.jpg"
alice <# "/f @bob ./tests/fixtures/test.jpg"
@@ -313,7 +313,7 @@ testGroupSendImage =
\alice bob cath -> withXFTPServer $ do
createGroup3 "team" alice bob cath
threadDelay 1000000
alice ##> "/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}"
alice ##> "/_send #1 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]"
alice <# "/f #team ./tests/fixtures/test.jpg"
alice <## "use /fc 1 to cancel sending"
concurrentlyN_
@@ -361,7 +361,7 @@ testGroupSendImageWithTextAndQuote =
(cath <# "#team bob> hi team")
threadDelay 1000000
msgItemId <- lastItemId alice
alice ##> ("/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> msgItemId <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}")
alice ##> ("/_send #1 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"quotedItemId\": " <> msgItemId <> ", \"msgContent\": {\"text\":\"hey bob\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]")
alice <# "#team > bob hi team"
alice <## " hey bob"
alice <# "/f #team ./tests/fixtures/test.jpg"
@@ -460,7 +460,7 @@ testXFTPFileTransferEncrypted =
let fileJSON = LB.unpack $ J.encode $ CryptoFile srcPath $ Just cfArgs
withXFTPServer $ do
connectUsers alice bob
alice ##> ("/_send @2 json {\"msgContent\":{\"type\":\"file\", \"text\":\"\"}, \"fileSource\": " <> fileJSON <> "}")
alice ##> ("/_send @2 json [{\"msgContent\":{\"type\":\"file\", \"text\":\"\"}, \"fileSource\": " <> fileJSON <> "}]")
alice <# "/f @bob ./tests/tmp/alice/test.pdf"
alice <## "use /fc 1 to cancel sending"
bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)"

Some files were not shown because too many files have changed in this diff Show More