mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ab5e7d02f | |||
| 29cc2cfb0c | |||
| 4826da0825 | |||
| 4f9f2cecec | |||
| a5c1be3b8f | |||
| f7cac769df | |||
| a4b086ce7a | |||
| 2a20afeda6 | |||
| 7b48c59f9f | |||
| 04033fc0b5 | |||
| 7b90e01b3a | |||
| beefb1ba12 | |||
| 6c1a821304 | |||
| ec40c2d7e5 | |||
| d6666b270f | |||
| 950b87b48f | |||
| ebbafc12d4 | |||
| 8053d8fc39 | |||
| d085a047fc | |||
| ef1897f865 | |||
| f587179045 | |||
| 791489e943 | |||
| 99d5a183c4 | |||
| c485837910 | |||
| 8979306e68 | |||
| e5a60b963e |
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
-2
@@ -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")
|
||||
|
||||
+1
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -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()
|
||||
}
|
||||
|
||||
+2
-8
@@ -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
|
||||
|
||||
+51
-56
@@ -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,
|
||||
|
||||
-1
@@ -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,
|
||||
|
||||
-1
@@ -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,
|
||||
|
||||
-2
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+5
-5
@@ -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)
|
||||
}
|
||||
|
||||
+1
-2
@@ -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))
|
||||
}
|
||||
|
||||
+26
-20
@@ -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 ->
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
+3
-5
@@ -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,
|
||||
|
||||
+4
-11
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
-1
@@ -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 -> {
|
||||
|
||||
+1
-2
@@ -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 {
|
||||
|
||||
+1
-3
@@ -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)))
|
||||
|
||||
+3
-13
@@ -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]
|
||||
* */
|
||||
|
||||
+3
-3
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+113
-112
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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)
|
||||
|
||||
+7
-5
@@ -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)
|
||||
|
||||
+2
-2
@@ -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))
|
||||
|
||||
+4
-1
@@ -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()
|
||||
|
||||
+2
-1
@@ -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),
|
||||
|
||||
+20
-16
@@ -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(
|
||||
|
||||
+1
-3
@@ -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)
|
||||
}
|
||||
|
||||
+2
-1
@@ -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()
|
||||
|
||||
+13
-85
@@ -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 {
|
||||
|
||||
+1
-2
@@ -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 -> {}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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 ()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user