mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
105 Commits
iOS
...
ab/freeze-test
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e3bad865c | |||
| 49b460226b | |||
| 6facc752d4 | |||
| 7976121bcc | |||
| 65dd52bfad | |||
| 59541a20f4 | |||
| cc998e89b3 | |||
| 8f7bd72832 | |||
| b8eaa7aaf9 | |||
| 52247e4719 | |||
| 883b2a917a | |||
| 8dfc2a149d | |||
| 8d42f3fd6c | |||
| b32751c584 | |||
| cb72c09528 | |||
| 302dda8cd6 | |||
| e037bb8fb3 | |||
| 951cb273ac | |||
| f8a469488e | |||
| f94c0311c1 | |||
| e1ff7c88d7 | |||
| 3b5e806418 | |||
| 9a1c7f41f7 | |||
| 40e69ae713 | |||
| b74e33b958 | |||
| 540c8883a0 | |||
| 0e18b13bea | |||
| ef5c13b1c1 | |||
| a4b44254bc | |||
| 38533213d2 | |||
| 5819e42305 | |||
| 9580b4110d | |||
| 05a64c99a2 | |||
| 6a21d5c7f1 | |||
| 950bbe19da | |||
| 05278e5a06 | |||
| b0002fe07d | |||
| 7a54d74517 | |||
| bfcb2ac230 | |||
| 3073c4a1d5 | |||
| d4ac1c0cf2 | |||
| 4a311b9578 | |||
| b8da5e225b | |||
| d29f1bb0cf | |||
| 75c2de8a12 | |||
| f20ac33e67 | |||
| 5cc537f14c | |||
| 0c096e2c89 | |||
| 1781495ee3 | |||
| 45102442f4 | |||
| 3bdc6b5e28 | |||
| 3597d34716 | |||
| 55ead740cc | |||
| 49a9b0e7d6 | |||
| 307a1b3c5e | |||
| ed6b3bbead | |||
| 7d4127c51d | |||
| e1a8099474 | |||
| daa8d9bb21 | |||
| 3937ffa9a6 | |||
| f6e66f1c53 | |||
| 1570bc2b99 | |||
| f3014f258d | |||
| 74b78a8d7b | |||
| fe4eb7b5af | |||
| 2516d5a393 | |||
| 4253cd7fb9 | |||
| 1f5b80d560 | |||
| 2de111e76c | |||
| 5dbe2b2745 | |||
| 6881600e06 | |||
| 9ded1c9821 | |||
| c3e82a6a4e | |||
| e7e66ff873 | |||
| d6b9a45a39 | |||
| 4004aafbc5 | |||
| c7a8992043 | |||
| ed9f277421 | |||
| d8fb31f167 | |||
| 7692195bfa | |||
| effc281271 | |||
| 67d74a0a27 | |||
| 74d186af16 | |||
| 4782cab507 | |||
| 2501cbe55d | |||
| 6b8b9ab4fd | |||
| 316d605899 | |||
| a3f2d5c919 | |||
| 0312fde818 | |||
| 915b53054c | |||
| e273bd1239 | |||
| e7f0234134 | |||
| 98a3fc214d | |||
| dacc075fe8 | |||
| f2b5c0f3a8 | |||
| 8e045764df | |||
| 81bd7d97c5 | |||
| 8f57925067 | |||
| 9bf99db82e | |||
| 5615cdbf1a | |||
| d802ae0058 | |||
| 8f2278198c | |||
| 10937a5a4e | |||
| 6aff6e9804 | |||
| 95477cae7e |
@@ -79,10 +79,10 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Haskell
|
- name: Setup Haskell
|
||||||
uses: haskell-actions/setup@v2
|
uses: haskell/actions/setup@v2
|
||||||
with:
|
with:
|
||||||
ghc-version: "9.6.3"
|
ghc-version: "8.10.7"
|
||||||
cabal-version: "3.10.1.0"
|
cabal-version: "latest"
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
@@ -188,7 +188,7 @@ jobs:
|
|||||||
APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }}
|
APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }}
|
||||||
APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }}
|
APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
scripts/ci/build-desktop-mac.sh
|
scripts/build-desktop-mac.sh
|
||||||
path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg)
|
path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg)
|
||||||
echo "package_path=$path" >> $GITHUB_OUTPUT
|
echo "package_path=$path" >> $GITHUB_OUTPUT
|
||||||
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT
|
||||||
@@ -259,7 +259,9 @@ jobs:
|
|||||||
# Unix /
|
# Unix /
|
||||||
|
|
||||||
# / Windows
|
# / Windows
|
||||||
# rm -rf dist-newstyle/src/direct-sq* is here because of the bug in cabal's dependency which prevents second build from finishing
|
|
||||||
|
# * In powershell multiline commands do not fail if individual commands fail - https://github.community/t/multiline-commands-on-windows-do-not-fail-if-individual-commands-fail/16753
|
||||||
|
# * And GitHub Actions does not support parameterizing shell in a matrix job - https://github.community/t/using-matrix-to-specify-shell-is-it-possible/17065
|
||||||
|
|
||||||
- name: 'Setup MSYS2'
|
- name: 'Setup MSYS2'
|
||||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
|
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ final class ChatModel: ObservableObject {
|
|||||||
// current WebRTC call
|
// current WebRTC call
|
||||||
@Published var callInvitations: Dictionary<ChatId, RcvCallInvitation> = [:]
|
@Published var callInvitations: Dictionary<ChatId, RcvCallInvitation> = [:]
|
||||||
@Published var activeCall: Call?
|
@Published var activeCall: Call?
|
||||||
@Published var callCommand: WCallCommand?
|
let callCommand: WebRTCCommandProcessor = WebRTCCommandProcessor()
|
||||||
@Published var showCallView = false
|
@Published var showCallView = false
|
||||||
// remote desktop
|
// remote desktop
|
||||||
@Published var remoteCtrlSession: RemoteCtrlSession?
|
@Published var remoteCtrlSession: RemoteCtrlSession?
|
||||||
@@ -267,7 +267,20 @@ final class ChatModel: ObservableObject {
|
|||||||
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||||
// update previews
|
// update previews
|
||||||
if let i = getChatIndex(cInfo.id) {
|
if let i = getChatIndex(cInfo.id) {
|
||||||
chats[i].chatItems = [cItem]
|
chats[i].chatItems = switch cInfo {
|
||||||
|
case .group:
|
||||||
|
if let currentPreviewItem = chats[i].chatItems.first {
|
||||||
|
if cItem.meta.itemTs >= currentPreviewItem.meta.itemTs {
|
||||||
|
[cItem]
|
||||||
|
} else {
|
||||||
|
[currentPreviewItem]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
[cItem]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
[cItem]
|
||||||
|
}
|
||||||
if case .rcvNew = cItem.meta.itemStatus {
|
if case .rcvNew = cItem.meta.itemStatus {
|
||||||
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1
|
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1
|
||||||
increaseUnreadCounter(user: currentUser!)
|
increaseUnreadCounter(user: currentUser!)
|
||||||
|
|||||||
@@ -605,27 +605,29 @@ func apiConnectPlan(connReq: String) async throws -> ConnectionPlan {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiConnect(incognito: Bool, connReq: String) async -> ConnReqType? {
|
func apiConnect(incognito: Bool, connReq: String) async -> (ConnReqType, PendingContactConnection)? {
|
||||||
let (connReqType, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
|
let (r, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
|
||||||
if let alert = alert {
|
if let alert = alert {
|
||||||
AlertManager.shared.showAlert(alert)
|
AlertManager.shared.showAlert(alert)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
return connReqType
|
return r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert?) {
|
func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, PendingContactConnection)?, Alert?) {
|
||||||
guard let userId = ChatModel.shared.currentUser?.userId else {
|
guard let userId = ChatModel.shared.currentUser?.userId else {
|
||||||
logger.error("apiConnect: no current user")
|
logger.error("apiConnect: no current user")
|
||||||
return (nil, nil)
|
return (nil, nil)
|
||||||
}
|
}
|
||||||
let r = await chatSendCmd(.apiConnect(userId: userId, incognito: incognito, connReq: connReq))
|
let r = await chatSendCmd(.apiConnect(userId: userId, incognito: incognito, connReq: connReq))
|
||||||
|
let m = ChatModel.shared
|
||||||
switch r {
|
switch r {
|
||||||
case .sentConfirmation: return (.invitation, nil)
|
case let .sentConfirmation(_, connection):
|
||||||
case .sentInvitation: return (.contact, nil)
|
return ((.invitation, connection), nil)
|
||||||
|
case let .sentInvitation(_, connection):
|
||||||
|
return ((.contact, connection), nil)
|
||||||
case let .contactAlreadyExists(_, contact):
|
case let .contactAlreadyExists(_, contact):
|
||||||
let m = ChatModel.shared
|
|
||||||
if let c = m.getContactChat(contact.contactId) {
|
if let c = m.getContactChat(contact.contactId) {
|
||||||
await MainActor.run { m.chatId = c.id }
|
await MainActor.run { m.chatId = c.id }
|
||||||
}
|
}
|
||||||
@@ -1362,18 +1364,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
logger.debug("processReceivedMsg: \(res.responseType)")
|
logger.debug("processReceivedMsg: \(res.responseType)")
|
||||||
switch res {
|
switch res {
|
||||||
case let .newContactConnection(user, connection):
|
|
||||||
if active(user) {
|
|
||||||
await MainActor.run {
|
|
||||||
m.updateContactConnection(connection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case let .contactConnectionDeleted(user, connection):
|
|
||||||
if active(user) {
|
|
||||||
await MainActor.run {
|
|
||||||
m.removeChat(connection.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case let .contactDeletedByContact(user, contact):
|
case let .contactDeletedByContact(user, contact):
|
||||||
if active(user) && contact.directOrUsed {
|
if active(user) && contact.directOrUsed {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
@@ -1666,36 +1656,40 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
activateCall(invitation)
|
activateCall(invitation)
|
||||||
case let .callOffer(_, contact, callType, offer, sharedKey, _):
|
case let .callOffer(_, contact, callType, offer, sharedKey, _):
|
||||||
await withCall(contact) { call in
|
await withCall(contact) { call in
|
||||||
call.callState = .offerReceived
|
await MainActor.run {
|
||||||
call.peerMedia = callType.media
|
call.callState = .offerReceived
|
||||||
call.sharedKey = sharedKey
|
call.peerMedia = callType.media
|
||||||
|
call.sharedKey = sharedKey
|
||||||
|
}
|
||||||
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
||||||
let iceServers = getIceServers()
|
let iceServers = getIceServers()
|
||||||
logger.debug(".callOffer useRelay \(useRelay)")
|
logger.debug(".callOffer useRelay \(useRelay)")
|
||||||
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
|
logger.debug(".callOffer iceServers \(String(describing: iceServers))")
|
||||||
m.callCommand = .offer(
|
await m.callCommand.processCommand(.offer(
|
||||||
offer: offer.rtcSession,
|
offer: offer.rtcSession,
|
||||||
iceCandidates: offer.rtcIceCandidates,
|
iceCandidates: offer.rtcIceCandidates,
|
||||||
media: callType.media, aesKey: sharedKey,
|
media: callType.media, aesKey: sharedKey,
|
||||||
iceServers: iceServers,
|
iceServers: iceServers,
|
||||||
relay: useRelay
|
relay: useRelay
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
case let .callAnswer(_, contact, answer):
|
case let .callAnswer(_, contact, answer):
|
||||||
await withCall(contact) { call in
|
await withCall(contact) { call in
|
||||||
call.callState = .answerReceived
|
await MainActor.run {
|
||||||
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
|
call.callState = .answerReceived
|
||||||
|
}
|
||||||
|
await m.callCommand.processCommand(.answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates))
|
||||||
}
|
}
|
||||||
case let .callExtraInfo(_, contact, extraInfo):
|
case let .callExtraInfo(_, contact, extraInfo):
|
||||||
await withCall(contact) { _ in
|
await withCall(contact) { _ in
|
||||||
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
|
await m.callCommand.processCommand(.ice(iceCandidates: extraInfo.rtcIceCandidates))
|
||||||
}
|
}
|
||||||
case let .callEnded(_, contact):
|
case let .callEnded(_, contact):
|
||||||
if let invitation = await MainActor.run(body: { m.callInvitations.removeValue(forKey: contact.id) }) {
|
if let invitation = await MainActor.run(body: { m.callInvitations.removeValue(forKey: contact.id) }) {
|
||||||
CallController.shared.reportCallRemoteEnded(invitation: invitation)
|
CallController.shared.reportCallRemoteEnded(invitation: invitation)
|
||||||
}
|
}
|
||||||
await withCall(contact) { call in
|
await withCall(contact) { call in
|
||||||
m.callCommand = .end
|
await m.callCommand.processCommand(.end)
|
||||||
CallController.shared.reportCallRemoteEnded(call: call)
|
CallController.shared.reportCallRemoteEnded(call: call)
|
||||||
}
|
}
|
||||||
case .chatSuspended:
|
case .chatSuspended:
|
||||||
@@ -1753,9 +1747,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
logger.debug("unsupported event: \(res.responseType)")
|
logger.debug("unsupported event: \(res.responseType)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func withCall(_ contact: Contact, _ perform: (Call) -> Void) async {
|
func withCall(_ contact: Contact, _ perform: (Call) async -> Void) async {
|
||||||
if let call = m.activeCall, call.contact.apiId == contact.apiId {
|
if let call = m.activeCall, call.contact.apiId == contact.apiId {
|
||||||
await MainActor.run { perform(call) }
|
await perform(call)
|
||||||
} else {
|
} else {
|
||||||
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
|
logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ struct ActiveCallView: View {
|
|||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
logger.debug("ActiveCallView: disappear")
|
logger.debug("ActiveCallView: disappear")
|
||||||
|
Task { await m.callCommand.setClient(nil) }
|
||||||
AppDelegate.keepScreenOn(false)
|
AppDelegate.keepScreenOn(false)
|
||||||
client?.endCall()
|
client?.endCall()
|
||||||
}
|
}
|
||||||
.onChange(of: m.callCommand) { _ in sendCommandToClient()}
|
|
||||||
.background(.black)
|
.background(.black)
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
}
|
}
|
||||||
@@ -60,19 +60,8 @@ struct ActiveCallView: View {
|
|||||||
private func createWebRTCClient() {
|
private func createWebRTCClient() {
|
||||||
if client == nil && canConnectCall {
|
if client == nil && canConnectCall {
|
||||||
client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
|
client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
|
||||||
sendCommandToClient()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sendCommandToClient() {
|
|
||||||
if call == m.activeCall,
|
|
||||||
m.activeCall != nil,
|
|
||||||
let client = client,
|
|
||||||
let cmd = m.callCommand {
|
|
||||||
m.callCommand = nil
|
|
||||||
logger.debug("sendCallCommand: \(cmd.cmdType)")
|
|
||||||
Task {
|
Task {
|
||||||
await client.sendCallCommand(command: cmd)
|
await m.callCommand.setClient(client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,8 +157,10 @@ struct ActiveCallView: View {
|
|||||||
}
|
}
|
||||||
case let .error(message):
|
case let .error(message):
|
||||||
logger.debug("ActiveCallView: command error: \(message)")
|
logger.debug("ActiveCallView: command error: \(message)")
|
||||||
|
AlertManager.shared.showAlert(Alert(title: Text("Error"), message: Text(message)))
|
||||||
case let .invalid(type):
|
case let .invalid(type):
|
||||||
logger.debug("ActiveCallView: invalid response: \(type)")
|
logger.debug("ActiveCallView: invalid response: \(type)")
|
||||||
|
AlertManager.shared.showAlert(Alert(title: Text("Invalid response"), message: Text(type)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,7 +246,6 @@ struct ActiveCallOverlay: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Text(call.encryptionStatus)
|
Text(call.encryptionStatus)
|
||||||
if let connInfo = call.connectionInfo {
|
if let connInfo = call.connectionInfo {
|
||||||
// Text("(") + Text(connInfo.text) + Text(", \(connInfo.protocolText))")
|
|
||||||
Text("(") + Text(connInfo.text) + Text(")")
|
Text("(") + Text(connInfo.text) + Text(")")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class CallManager {
|
|||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
if let call = m.activeCall, call.callkitUUID == callUUID {
|
if let call = m.activeCall, call.callkitUUID == callUUID {
|
||||||
m.showCallView = true
|
m.showCallView = true
|
||||||
m.callCommand = .capabilities(media: call.localMedia)
|
Task { await m.callCommand.processCommand(.capabilities(media: call.localMedia)) }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -57,19 +57,21 @@ class CallManager {
|
|||||||
m.activeCall = call
|
m.activeCall = call
|
||||||
m.showCallView = true
|
m.showCallView = true
|
||||||
|
|
||||||
m.callCommand = .start(
|
Task {
|
||||||
|
await m.callCommand.processCommand(.start(
|
||||||
media: invitation.callType.media,
|
media: invitation.callType.media,
|
||||||
aesKey: invitation.sharedKey,
|
aesKey: invitation.sharedKey,
|
||||||
iceServers: iceServers,
|
iceServers: iceServers,
|
||||||
relay: useRelay
|
relay: useRelay
|
||||||
)
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool {
|
func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool {
|
||||||
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
|
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
|
||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
m.callCommand = .media(media: media, enable: enable)
|
Task { await m.callCommand.processCommand(.media(media: media, enable: enable)) }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -94,11 +96,13 @@ class CallManager {
|
|||||||
completed()
|
completed()
|
||||||
} else {
|
} else {
|
||||||
logger.debug("CallManager.endCall: ending call...")
|
logger.debug("CallManager.endCall: ending call...")
|
||||||
m.callCommand = .end
|
|
||||||
m.activeCall = nil
|
|
||||||
m.showCallView = false
|
|
||||||
completed()
|
|
||||||
Task {
|
Task {
|
||||||
|
await m.callCommand.processCommand(.end)
|
||||||
|
await MainActor.run {
|
||||||
|
m.activeCall = nil
|
||||||
|
m.showCallView = false
|
||||||
|
completed()
|
||||||
|
}
|
||||||
do {
|
do {
|
||||||
try await apiEndCall(call.contact)
|
try await apiEndCall(call.contact)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -335,6 +335,50 @@ extension WCallResponse: Encodable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actor WebRTCCommandProcessor {
|
||||||
|
private var client: WebRTCClient? = nil
|
||||||
|
private var commands: [WCallCommand] = []
|
||||||
|
private var running: Bool = false
|
||||||
|
|
||||||
|
func setClient(_ client: WebRTCClient?) async {
|
||||||
|
logger.debug("WebRTC: setClient, commands count \(self.commands.count)")
|
||||||
|
self.client = client
|
||||||
|
if client != nil {
|
||||||
|
await processAllCommands()
|
||||||
|
} else {
|
||||||
|
commands.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processCommand(_ c: WCallCommand) async {
|
||||||
|
// logger.debug("WebRTC: process command \(c.cmdType)")
|
||||||
|
commands.append(c)
|
||||||
|
if !running && client != nil {
|
||||||
|
await processAllCommands()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processAllCommands() async {
|
||||||
|
logger.debug("WebRTC: process all commands, commands count \(self.commands.count), client == nil \(self.client == nil)")
|
||||||
|
if let client = client {
|
||||||
|
running = true
|
||||||
|
while let c = commands.first, shouldRunCommand(client, c) {
|
||||||
|
commands.remove(at: 0)
|
||||||
|
await client.sendCallCommand(command: c)
|
||||||
|
logger.debug("WebRTC: processed cmd \(c.cmdType)")
|
||||||
|
}
|
||||||
|
running = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRunCommand(_ client: WebRTCClient, _ c: WCallCommand) -> Bool {
|
||||||
|
switch c {
|
||||||
|
case .capabilities, .start, .offer, .end: true
|
||||||
|
default: client.activeCall.wrappedValue != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ConnectionState: Codable, Equatable {
|
struct ConnectionState: Codable, Equatable {
|
||||||
var connectionState: String
|
var connectionState: String
|
||||||
var iceConnectionState: String
|
var iceConnectionState: String
|
||||||
@@ -358,26 +402,12 @@ struct ConnectionInfo: Codable, Equatable {
|
|||||||
return "\(local?.rawValue ?? unknown) / \(remote?.rawValue ?? unknown)"
|
return "\(local?.rawValue ?? unknown) / \(remote?.rawValue ?? unknown)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var protocolText: String {
|
|
||||||
let unknown = NSLocalizedString("unknown", comment: "connection info")
|
|
||||||
let local = localCandidate?.protocol?.uppercased() ?? unknown
|
|
||||||
let localRelay = localCandidate?.relayProtocol?.uppercased() ?? unknown
|
|
||||||
let remote = remoteCandidate?.protocol?.uppercased() ?? unknown
|
|
||||||
let localText = localRelay == local || localCandidate?.relayProtocol == nil
|
|
||||||
? local
|
|
||||||
: "\(local) (\(localRelay))"
|
|
||||||
return local == remote
|
|
||||||
? localText
|
|
||||||
: "\(localText) / \(remote)"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
||||||
struct RTCIceCandidate: Codable, Equatable {
|
struct RTCIceCandidate: Codable, Equatable {
|
||||||
var candidateType: RTCIceCandidateType?
|
var candidateType: RTCIceCandidateType?
|
||||||
var `protocol`: String?
|
var `protocol`: String?
|
||||||
var relayProtocol: String?
|
|
||||||
var sdpMid: String?
|
var sdpMid: String?
|
||||||
var sdpMLineIndex: Int?
|
var sdpMLineIndex: Int?
|
||||||
var candidate: String
|
var candidate: String
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
|
|
||||||
struct Call {
|
struct Call {
|
||||||
var connection: RTCPeerConnection
|
var connection: RTCPeerConnection
|
||||||
var iceCandidates: [RTCIceCandidate]
|
var iceCandidates: IceCandidates
|
||||||
var localMedia: CallMediaType
|
var localMedia: CallMediaType
|
||||||
var localCamera: RTCVideoCapturer?
|
var localCamera: RTCVideoCapturer?
|
||||||
var localVideoSource: RTCVideoSource?
|
var localVideoSource: RTCVideoSource?
|
||||||
@@ -33,10 +33,24 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
var frameDecryptor: RTCFrameDecryptor?
|
var frameDecryptor: RTCFrameDecryptor?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actor IceCandidates {
|
||||||
|
private var candidates: [RTCIceCandidate] = []
|
||||||
|
|
||||||
|
func getAndClear() async -> [RTCIceCandidate] {
|
||||||
|
let cs = candidates
|
||||||
|
candidates = []
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
func append(_ c: RTCIceCandidate) async {
|
||||||
|
candidates.append(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private let rtcAudioSession = RTCAudioSession.sharedInstance()
|
private let rtcAudioSession = RTCAudioSession.sharedInstance()
|
||||||
private let audioQueue = DispatchQueue(label: "audio")
|
private let audioQueue = DispatchQueue(label: "audio")
|
||||||
private var sendCallResponse: (WVAPIMessage) async -> Void
|
private var sendCallResponse: (WVAPIMessage) async -> Void
|
||||||
private var activeCall: Binding<Call?>
|
var activeCall: Binding<Call?>
|
||||||
private var localRendererAspectRatio: Binding<CGFloat?>
|
private var localRendererAspectRatio: Binding<CGFloat?>
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
@@ -60,7 +74,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"),
|
WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"),
|
||||||
]
|
]
|
||||||
|
|
||||||
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ remoteIceCandidates: [RTCIceCandidate], _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
|
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
|
||||||
let connection = createPeerConnection(iceServers ?? getWebRTCIceServers() ?? defaultIceServers, relay)
|
let connection = createPeerConnection(iceServers ?? getWebRTCIceServers() ?? defaultIceServers, relay)
|
||||||
connection.delegate = self
|
connection.delegate = self
|
||||||
createAudioSender(connection)
|
createAudioSender(connection)
|
||||||
@@ -87,7 +101,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
}
|
}
|
||||||
return Call(
|
return Call(
|
||||||
connection: connection,
|
connection: connection,
|
||||||
iceCandidates: remoteIceCandidates,
|
iceCandidates: IceCandidates(),
|
||||||
localMedia: mediaType,
|
localMedia: mediaType,
|
||||||
localCamera: localCamera,
|
localCamera: localCamera,
|
||||||
localVideoSource: localVideoSource,
|
localVideoSource: localVideoSource,
|
||||||
@@ -144,26 +158,18 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
logger.debug("starting incoming call - create webrtc session")
|
logger.debug("starting incoming call - create webrtc session")
|
||||||
if activeCall.wrappedValue != nil { endCall() }
|
if activeCall.wrappedValue != nil { endCall() }
|
||||||
let encryption = WebRTCClient.enableEncryption
|
let encryption = WebRTCClient.enableEncryption
|
||||||
let call = initializeCall(iceServers?.toWebRTCIceServers(), [], media, encryption ? aesKey : nil, relay)
|
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, encryption ? aesKey : nil, relay)
|
||||||
activeCall.wrappedValue = call
|
activeCall.wrappedValue = call
|
||||||
call.connection.offer { answer in
|
let (offer, error) = await call.connection.offer()
|
||||||
Task {
|
if let offer = offer {
|
||||||
let gotCandidates = await self.waitWithTimeout(10_000, stepMs: 1000, until: { self.activeCall.wrappedValue?.iceCandidates.count ?? 0 > 0 })
|
resp = .offer(
|
||||||
if gotCandidates {
|
offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: offer.type.toSdpType(), sdp: offer.sdp))),
|
||||||
await self.sendCallResponse(.init(
|
iceCandidates: compressToBase64(input: encodeJSON(await self.getInitialIceCandidates())),
|
||||||
corrId: nil,
|
capabilities: CallCapabilities(encryption: encryption)
|
||||||
resp: .offer(
|
)
|
||||||
offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
|
self.waitForMoreIceCandidates()
|
||||||
iceCandidates: compressToBase64(input: encodeJSON(self.activeCall.wrappedValue?.iceCandidates ?? [])),
|
} else {
|
||||||
capabilities: CallCapabilities(encryption: encryption)
|
resp = .error(message: "offer error: \(error?.localizedDescription ?? "unknown error")")
|
||||||
),
|
|
||||||
command: command)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
self.endCall()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay):
|
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay):
|
||||||
if activeCall.wrappedValue != nil {
|
if activeCall.wrappedValue != nil {
|
||||||
@@ -172,26 +178,21 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
resp = .error(message: "accept: encryption is not supported")
|
resp = .error(message: "accept: encryption is not supported")
|
||||||
} else if let offer: CustomRTCSessionDescription = decodeJSON(decompressFromBase64(input: offer)),
|
} else if let offer: CustomRTCSessionDescription = decodeJSON(decompressFromBase64(input: offer)),
|
||||||
let remoteIceCandidates: [RTCIceCandidate] = decodeJSON(decompressFromBase64(input: iceCandidates)) {
|
let remoteIceCandidates: [RTCIceCandidate] = decodeJSON(decompressFromBase64(input: iceCandidates)) {
|
||||||
let call = initializeCall(iceServers?.toWebRTCIceServers(), remoteIceCandidates, media, WebRTCClient.enableEncryption ? aesKey : nil, relay)
|
let call = initializeCall(iceServers?.toWebRTCIceServers(), media, WebRTCClient.enableEncryption ? aesKey : nil, relay)
|
||||||
activeCall.wrappedValue = call
|
activeCall.wrappedValue = call
|
||||||
let pc = call.connection
|
let pc = call.connection
|
||||||
if let type = offer.type, let sdp = offer.sdp {
|
if let type = offer.type, let sdp = offer.sdp {
|
||||||
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
|
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
|
||||||
pc.answer { answer in
|
let (answer, error) = await pc.answer()
|
||||||
|
if let answer = answer {
|
||||||
self.addIceCandidates(pc, remoteIceCandidates)
|
self.addIceCandidates(pc, remoteIceCandidates)
|
||||||
// Task {
|
resp = .answer(
|
||||||
// try? await Task.sleep(nanoseconds: 32_000 * 1000000)
|
answer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
|
||||||
Task {
|
iceCandidates: compressToBase64(input: encodeJSON(await self.getInitialIceCandidates()))
|
||||||
await self.sendCallResponse(.init(
|
)
|
||||||
corrId: nil,
|
self.waitForMoreIceCandidates()
|
||||||
resp: .answer(
|
} else {
|
||||||
answer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
|
resp = .error(message: "answer error: \(error?.localizedDescription ?? "unknown error")")
|
||||||
iceCandidates: compressToBase64(input: encodeJSON(call.iceCandidates))
|
|
||||||
),
|
|
||||||
command: command)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resp = .error(message: "accept: remote description is not set")
|
resp = .error(message: "accept: remote description is not set")
|
||||||
@@ -234,6 +235,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
resp = .ok
|
resp = .ok
|
||||||
}
|
}
|
||||||
case .end:
|
case .end:
|
||||||
|
// TODO possibly, endCall should be called before returning .ok
|
||||||
await sendCallResponse(.init(corrId: nil, resp: .ok, command: command))
|
await sendCallResponse(.init(corrId: nil, resp: .ok, command: command))
|
||||||
endCall()
|
endCall()
|
||||||
}
|
}
|
||||||
@@ -242,6 +244,33 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getInitialIceCandidates() async -> [RTCIceCandidate] {
|
||||||
|
await untilIceComplete(timeoutMs: 750, stepMs: 150) {}
|
||||||
|
let candidates = await activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
|
||||||
|
logger.debug("WebRTCClient: sending initial ice candidates: \(candidates.count)")
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForMoreIceCandidates() {
|
||||||
|
Task {
|
||||||
|
await untilIceComplete(timeoutMs: 12000, stepMs: 1500) {
|
||||||
|
let candidates = await self.activeCall.wrappedValue?.iceCandidates.getAndClear() ?? []
|
||||||
|
if candidates.count > 0 {
|
||||||
|
logger.debug("WebRTCClient: sending more ice candidates: \(candidates.count)")
|
||||||
|
await self.sendIceCandidates(candidates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendIceCandidates(_ candidates: [RTCIceCandidate]) async {
|
||||||
|
await self.sendCallResponse(.init(
|
||||||
|
corrId: nil,
|
||||||
|
resp: .ice(iceCandidates: compressToBase64(input: encodeJSON(candidates))),
|
||||||
|
command: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func enableMedia(_ media: CallMediaType, _ enable: Bool) {
|
func enableMedia(_ media: CallMediaType, _ enable: Bool) {
|
||||||
logger.debug("WebRTCClient: enabling media \(media.rawValue) \(enable)")
|
logger.debug("WebRTCClient: enabling media \(media.rawValue) \(enable)")
|
||||||
media == .video ? setVideoEnabled(enable) : setAudioEnabled(enable)
|
media == .video ? setVideoEnabled(enable) : setAudioEnabled(enable)
|
||||||
@@ -387,12 +416,13 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
|
|||||||
audioSessionToDefaults()
|
audioSessionToDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitWithTimeout(_ timeoutMs: UInt64, stepMs: UInt64, until success: () -> Bool) async -> Bool {
|
func untilIceComplete(timeoutMs: UInt64, stepMs: UInt64, action: @escaping () async -> Void) async {
|
||||||
let startedAt = DispatchTime.now()
|
var t: UInt64 = 0
|
||||||
while !success() && startedAt.uptimeNanoseconds + timeoutMs * 1000000 > DispatchTime.now().uptimeNanoseconds {
|
repeat {
|
||||||
guard let _ = try? await Task.sleep(nanoseconds: stepMs * 1000000) else { break }
|
_ = try? await Task.sleep(nanoseconds: stepMs * 1000000)
|
||||||
}
|
t += stepMs
|
||||||
return success()
|
await action()
|
||||||
|
} while t < timeoutMs && activeCall.wrappedValue?.connection.iceGatheringState != .complete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,25 +435,33 @@ extension WebRTC.RTCPeerConnection {
|
|||||||
optionalConstraints: nil)
|
optionalConstraints: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func offer(_ completion: @escaping (_ sdp: RTCSessionDescription) -> Void) {
|
func offer() async -> (RTCSessionDescription?, Error?) {
|
||||||
offer(for: mediaConstraints()) { (sdp, error) in
|
await withCheckedContinuation { cont in
|
||||||
guard let sdp = sdp else {
|
offer(for: mediaConstraints()) { (sdp, error) in
|
||||||
return
|
self.processSDP(cont, sdp, error)
|
||||||
}
|
}
|
||||||
self.setLocalDescription(sdp, completionHandler: { (error) in
|
|
||||||
completion(sdp)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func answer(_ completion: @escaping (_ sdp: RTCSessionDescription) -> Void) {
|
func answer() async -> (RTCSessionDescription?, Error?) {
|
||||||
answer(for: mediaConstraints()) { (sdp, error) in
|
await withCheckedContinuation { cont in
|
||||||
guard let sdp = sdp else {
|
answer(for: mediaConstraints()) { (sdp, error) in
|
||||||
return
|
self.processSDP(cont, sdp, error)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processSDP(_ cont: CheckedContinuation<(RTCSessionDescription?, Error?), Never>, _ sdp: RTCSessionDescription?, _ error: Error?) {
|
||||||
|
if let sdp = sdp {
|
||||||
self.setLocalDescription(sdp, completionHandler: { (error) in
|
self.setLocalDescription(sdp, completionHandler: { (error) in
|
||||||
completion(sdp)
|
if let error = error {
|
||||||
|
cont.resume(returning: (nil, error))
|
||||||
|
} else {
|
||||||
|
cont.resume(returning: (sdp, nil))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
cont.resume(returning: (nil, error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,6 +517,7 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
|
|||||||
default: enableSpeaker = false
|
default: enableSpeaker = false
|
||||||
}
|
}
|
||||||
setSpeakerEnabledAndConfigureSession(enableSpeaker)
|
setSpeakerEnabledAndConfigureSession(enableSpeaker)
|
||||||
|
case .connected: sendConnectedEvent(connection)
|
||||||
case .disconnected, .failed: endCall()
|
case .disconnected, .failed: endCall()
|
||||||
default: do {}
|
default: do {}
|
||||||
}
|
}
|
||||||
@@ -491,7 +530,9 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
|
|||||||
|
|
||||||
func peerConnection(_ connection: RTCPeerConnection, didGenerate candidate: WebRTC.RTCIceCandidate) {
|
func peerConnection(_ connection: RTCPeerConnection, didGenerate candidate: WebRTC.RTCIceCandidate) {
|
||||||
// logger.debug("Connection generated candidate \(candidate.debugDescription)")
|
// logger.debug("Connection generated candidate \(candidate.debugDescription)")
|
||||||
activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil, nil))
|
Task {
|
||||||
|
await self.activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func peerConnection(_ connection: RTCPeerConnection, didRemove candidates: [WebRTC.RTCIceCandidate]) {
|
func peerConnection(_ connection: RTCPeerConnection, didRemove candidates: [WebRTC.RTCIceCandidate]) {
|
||||||
@@ -506,10 +547,9 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
|
|||||||
lastReceivedMs lastDataReceivedMs: Int32,
|
lastReceivedMs lastDataReceivedMs: Int32,
|
||||||
changeReason reason: String) {
|
changeReason reason: String) {
|
||||||
// logger.debug("Connection changed candidate \(reason) \(remote.debugDescription) \(remote.description)")
|
// logger.debug("Connection changed candidate \(reason) \(remote.debugDescription) \(remote.description)")
|
||||||
sendConnectedEvent(connection, local: local, remote: remote)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendConnectedEvent(_ connection: WebRTC.RTCPeerConnection, local: WebRTC.RTCIceCandidate, remote: WebRTC.RTCIceCandidate) {
|
func sendConnectedEvent(_ connection: WebRTC.RTCPeerConnection) {
|
||||||
connection.statistics { (stats: RTCStatisticsReport) in
|
connection.statistics { (stats: RTCStatisticsReport) in
|
||||||
stats.statistics.values.forEach { stat in
|
stats.statistics.values.forEach { stat in
|
||||||
// logger.debug("Stat \(stat.debugDescription)")
|
// logger.debug("Stat \(stat.debugDescription)")
|
||||||
@@ -517,24 +557,25 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
|
|||||||
let localId = stat.values["localCandidateId"] as? String,
|
let localId = stat.values["localCandidateId"] as? String,
|
||||||
let remoteId = stat.values["remoteCandidateId"] as? String,
|
let remoteId = stat.values["remoteCandidateId"] as? String,
|
||||||
let localStats = stats.statistics[localId],
|
let localStats = stats.statistics[localId],
|
||||||
let remoteStats = stats.statistics[remoteId],
|
let remoteStats = stats.statistics[remoteId]
|
||||||
local.sdp.contains("\((localStats.values["ip"] as? String ?? "--")) \((localStats.values["port"] as? String ?? "--"))") &&
|
|
||||||
remote.sdp.contains("\((remoteStats.values["ip"] as? String ?? "--")) \((remoteStats.values["port"] as? String ?? "--"))")
|
|
||||||
{
|
{
|
||||||
Task {
|
Task {
|
||||||
await self.sendCallResponse(.init(
|
await self.sendCallResponse(.init(
|
||||||
corrId: nil,
|
corrId: nil,
|
||||||
resp: .connected(connectionInfo: ConnectionInfo(
|
resp: .connected(connectionInfo: ConnectionInfo(
|
||||||
localCandidate: local.toCandidate(
|
localCandidate: RTCIceCandidate(
|
||||||
RTCIceCandidateType.init(rawValue: localStats.values["candidateType"] as! String),
|
candidateType: RTCIceCandidateType.init(rawValue: localStats.values["candidateType"] as! String),
|
||||||
localStats.values["protocol"] as? String,
|
protocol: localStats.values["protocol"] as? String,
|
||||||
localStats.values["relayProtocol"] as? String
|
sdpMid: nil,
|
||||||
|
sdpMLineIndex: nil,
|
||||||
|
candidate: ""
|
||||||
),
|
),
|
||||||
remoteCandidate: remote.toCandidate(
|
remoteCandidate: RTCIceCandidate(
|
||||||
RTCIceCandidateType.init(rawValue: remoteStats.values["candidateType"] as! String),
|
candidateType: RTCIceCandidateType.init(rawValue: remoteStats.values["candidateType"] as! String),
|
||||||
remoteStats.values["protocol"] as? String,
|
protocol: remoteStats.values["protocol"] as? String,
|
||||||
remoteStats.values["relayProtocol"] as? String
|
sdpMid: nil,
|
||||||
))),
|
sdpMLineIndex: nil,
|
||||||
|
candidate: ""))),
|
||||||
command: nil)
|
command: nil)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -634,11 +675,10 @@ extension RTCIceCandidate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension WebRTC.RTCIceCandidate {
|
extension WebRTC.RTCIceCandidate {
|
||||||
func toCandidate(_ candidateType: RTCIceCandidateType?, _ protocol: String?, _ relayProtocol: String?) -> RTCIceCandidate {
|
func toCandidate(_ candidateType: RTCIceCandidateType?, _ protocol: String?) -> RTCIceCandidate {
|
||||||
RTCIceCandidate(
|
RTCIceCandidate(
|
||||||
candidateType: candidateType,
|
candidateType: candidateType,
|
||||||
protocol: `protocol`,
|
protocol: `protocol`,
|
||||||
relayProtocol: relayProtocol,
|
|
||||||
sdpMid: sdpMid,
|
sdpMid: sdpMid,
|
||||||
sdpMLineIndex: Int(sdpMLineIndex),
|
sdpMLineIndex: Int(sdpMLineIndex),
|
||||||
candidate: sdp
|
candidate: sdp
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ struct CreateLinkView: View {
|
|||||||
Task {
|
Task {
|
||||||
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
|
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
m.updateContactConnection(pcc)
|
||||||
connReqInvitation = connReq
|
connReqInvitation = connReq
|
||||||
contactConnection = pcc
|
contactConnection = pcc
|
||||||
m.connReqInv = connReq
|
m.connReqInv = connReq
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ struct NewChatButton: View {
|
|||||||
func addContactAction() {
|
func addContactAction() {
|
||||||
Task {
|
Task {
|
||||||
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
|
if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) {
|
||||||
|
await MainActor.run {
|
||||||
|
ChatModel.shared.updateContactConnection(pcc)
|
||||||
|
}
|
||||||
actionSheet = .createLink(link: connReq, connection: pcc)
|
actionSheet = .createLink(link: connReq, connection: pcc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,7 +349,10 @@ private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incogn
|
|||||||
|
|
||||||
private func connectViaLink(_ connectionLink: String, connectionPlan: ConnectionPlan?, dismiss: Bool, incognito: Bool) {
|
private func connectViaLink(_ connectionLink: String, connectionPlan: ConnectionPlan?, dismiss: Bool, incognito: Bool) {
|
||||||
Task {
|
Task {
|
||||||
if let connReqType = await apiConnect(incognito: incognito, connReq: connectionLink) {
|
if let (connReqType, pcc) = await apiConnect(incognito: incognito, connReq: connectionLink) {
|
||||||
|
await MainActor.run {
|
||||||
|
ChatModel.shared.updateContactConnection(pcc)
|
||||||
|
}
|
||||||
let crt: ConnReqType
|
let crt: ConnReqType
|
||||||
if let plan = connectionPlan {
|
if let plan = connectionPlan {
|
||||||
crt = planToConnReqType(plan)
|
crt = planToConnReqType(plan)
|
||||||
|
|||||||
@@ -165,6 +165,11 @@
|
|||||||
64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */; };
|
64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */; };
|
||||||
64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DCB29FFE3E800E3D48D /* MailView.swift */; };
|
64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DCB29FFE3E800E3D48D /* MailView.swift */; };
|
||||||
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; };
|
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; };
|
||||||
|
6449333A2AF8E51000AC506E /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933352AF8E51000AC506E /* libgmpxx.a */; };
|
||||||
|
6449333B2AF8E51000AC506E /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933362AF8E51000AC506E /* libgmp.a */; };
|
||||||
|
6449333C2AF8E51000AC506E /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933372AF8E51000AC506E /* libffi.a */; };
|
||||||
|
6449333D2AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933382AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a */; };
|
||||||
|
6449333E2AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644933392AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a */; };
|
||||||
644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */; };
|
644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */; };
|
||||||
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */; };
|
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */; };
|
||||||
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; };
|
644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; };
|
||||||
@@ -448,6 +453,11 @@
|
|||||||
64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = "<group>"; };
|
64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = "<group>"; };
|
||||||
64466DCB29FFE3E800E3D48D /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = "<group>"; };
|
64466DCB29FFE3E800E3D48D /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = "<group>"; };
|
||||||
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = "<group>"; };
|
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = "<group>"; };
|
||||||
|
644933352AF8E51000AC506E /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||||
|
644933362AF8E51000AC506E /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||||
|
644933372AF8E51000AC506E /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||||
|
644933382AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||||
|
644933392AF8E51000AC506E /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a"; sourceTree = "<group>"; };
|
||||||
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeVoiceView.swift; sourceTree = "<group>"; };
|
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeVoiceView.swift; sourceTree = "<group>"; };
|
||||||
644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = "<group>"; };
|
644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = "<group>"; };
|
||||||
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = "<group>"; };
|
644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = "<group>"; };
|
||||||
|
|||||||
@@ -505,8 +505,8 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection)
|
case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection)
|
||||||
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
|
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
|
||||||
case connectionPlan(user: UserRef, connectionPlan: ConnectionPlan)
|
case connectionPlan(user: UserRef, connectionPlan: ConnectionPlan)
|
||||||
case sentConfirmation(user: UserRef)
|
case sentConfirmation(user: UserRef, connection: PendingContactConnection)
|
||||||
case sentInvitation(user: UserRef)
|
case sentInvitation(user: UserRef, connection: PendingContactConnection)
|
||||||
case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?)
|
case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?)
|
||||||
case contactAlreadyExists(user: UserRef, contact: Contact)
|
case contactAlreadyExists(user: UserRef, contact: Contact)
|
||||||
case contactRequestAlreadyAccepted(user: UserRef, contact: Contact)
|
case contactRequestAlreadyAccepted(user: UserRef, contact: Contact)
|
||||||
@@ -605,7 +605,6 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case ntfTokenStatus(status: NtfTknStatus)
|
case ntfTokenStatus(status: NtfTknStatus)
|
||||||
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
|
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
|
||||||
case ntfMessages(user_: User?, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
case ntfMessages(user_: User?, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
||||||
case newContactConnection(user: UserRef, connection: PendingContactConnection)
|
|
||||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||||
// remote desktop responses/events
|
// remote desktop responses/events
|
||||||
case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo])
|
case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo])
|
||||||
@@ -613,7 +612,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case remoteCtrlConnecting(remoteCtrl_: RemoteCtrlInfo?, ctrlAppInfo: CtrlAppInfo, appVersion: String)
|
case remoteCtrlConnecting(remoteCtrl_: RemoteCtrlInfo?, ctrlAppInfo: CtrlAppInfo, appVersion: String)
|
||||||
case remoteCtrlSessionCode(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String)
|
case remoteCtrlSessionCode(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String)
|
||||||
case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo)
|
case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo)
|
||||||
case remoteCtrlStopped
|
case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason)
|
||||||
// misc
|
// misc
|
||||||
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
|
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
|
||||||
case cmdOk(user: UserRef?)
|
case cmdOk(user: UserRef?)
|
||||||
@@ -752,7 +751,6 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case .ntfTokenStatus: return "ntfTokenStatus"
|
case .ntfTokenStatus: return "ntfTokenStatus"
|
||||||
case .ntfToken: return "ntfToken"
|
case .ntfToken: return "ntfToken"
|
||||||
case .ntfMessages: return "ntfMessages"
|
case .ntfMessages: return "ntfMessages"
|
||||||
case .newContactConnection: return "newContactConnection"
|
|
||||||
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
||||||
case .remoteCtrlList: return "remoteCtrlList"
|
case .remoteCtrlList: return "remoteCtrlList"
|
||||||
case .remoteCtrlFound: return "remoteCtrlFound"
|
case .remoteCtrlFound: return "remoteCtrlFound"
|
||||||
@@ -803,11 +801,11 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)")
|
case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)")
|
||||||
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
|
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
|
||||||
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
|
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
|
||||||
case let .invitation(u, connReqInvitation, _): return withUser(u, connReqInvitation)
|
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 .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
|
||||||
case let .connectionPlan(u, connectionPlan): return withUser(u, String(describing: connectionPlan))
|
case let .connectionPlan(u, connectionPlan): return withUser(u, String(describing: connectionPlan))
|
||||||
case .sentConfirmation: return noDetails
|
case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection))
|
||||||
case .sentInvitation: return noDetails
|
case let .sentInvitation(u, connection): return withUser(u, String(describing: connection))
|
||||||
case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact))
|
case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact))
|
||||||
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
|
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
|
||||||
case let .contactRequestAlreadyAccepted(u, contact): return withUser(u, String(describing: contact))
|
case let .contactRequestAlreadyAccepted(u, contact): return withUser(u, String(describing: contact))
|
||||||
@@ -900,7 +898,6 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .ntfTokenStatus(status): return String(describing: status)
|
case let .ntfTokenStatus(status): return String(describing: status)
|
||||||
case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
|
case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
|
||||||
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
|
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
|
||||||
case let .newContactConnection(u, connection): return withUser(u, String(describing: connection))
|
|
||||||
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||||
case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls)
|
case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls)
|
||||||
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)"
|
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)"
|
||||||
@@ -1552,6 +1549,13 @@ public enum RemoteCtrlSessionState: Decodable {
|
|||||||
case connected(sessionCode: String)
|
case connected(sessionCode: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum RemoteCtrlStopReason: Decodable {
|
||||||
|
case discoveryFailed(chatError: ChatError)
|
||||||
|
case connectionFailed(chatError: ChatError)
|
||||||
|
case setupFailed(chatError: ChatError)
|
||||||
|
case disconnected
|
||||||
|
}
|
||||||
|
|
||||||
public struct CtrlAppInfo: Decodable {
|
public struct CtrlAppInfo: Decodable {
|
||||||
public var appVersionRange: AppVersionRange
|
public var appVersionRange: AppVersionRange
|
||||||
public var deviceName: String
|
public var deviceName: String
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
}
|
}
|
||||||
Lifecycle.Event.ON_RESUME -> {
|
Lifecycle.Event.ON_RESUME -> {
|
||||||
isAppOnForeground = true
|
isAppOnForeground = true
|
||||||
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
|
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) {
|
||||||
SimplexService.showBackgroundServiceNoticeIfNeeded()
|
SimplexService.showBackgroundServiceNoticeIfNeeded()
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
+4
-5
@@ -370,7 +370,6 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
|
|||||||
InfoText(call.callState.text)
|
InfoText(call.callState.text)
|
||||||
|
|
||||||
val connInfo = call.connectionInfo
|
val connInfo = call.connectionInfo
|
||||||
// val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})"
|
|
||||||
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
|
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
|
||||||
InfoText(call.encryptionStatus + connInfoText)
|
InfoText(call.encryptionStatus + connInfoText)
|
||||||
}
|
}
|
||||||
@@ -585,8 +584,8 @@ fun PreviewActiveCallOverlayVideo() {
|
|||||||
localMedia = CallMediaType.Video,
|
localMedia = CallMediaType.Video,
|
||||||
peerMedia = CallMediaType.Video,
|
peerMedia = CallMediaType.Video,
|
||||||
connectionInfo = ConnectionInfo(
|
connectionInfo = ConnectionInfo(
|
||||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null),
|
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
|
||||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null)
|
RTCIceCandidate(RTCIceCandidateType.Host, "tcp")
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
speakerCanBeEnabled = true,
|
speakerCanBeEnabled = true,
|
||||||
@@ -611,8 +610,8 @@ fun PreviewActiveCallOverlayAudio() {
|
|||||||
localMedia = CallMediaType.Audio,
|
localMedia = CallMediaType.Audio,
|
||||||
peerMedia = CallMediaType.Audio,
|
peerMedia = CallMediaType.Audio,
|
||||||
connectionInfo = ConnectionInfo(
|
connectionInfo = ConnectionInfo(
|
||||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null),
|
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
|
||||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null)
|
RTCIceCandidate(RTCIceCandidateType.Host, "udp")
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
speakerCanBeEnabled = true,
|
speakerCanBeEnabled = true,
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package chat.simplex.common.views.onboarding
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import chat.simplex.common.model.SharedPreference
|
||||||
|
import chat.simplex.common.model.User
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)?) {
|
||||||
|
if (user == null) {
|
||||||
|
OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, onclick = onclick)
|
||||||
|
} else {
|
||||||
|
OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick = onclick)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ if(NOT APPLE)
|
|||||||
else()
|
else()
|
||||||
# Without direct linking it can't find hs_init in linking step
|
# Without direct linking it can't find hs_init in linking step
|
||||||
add_library( rts SHARED IMPORTED )
|
add_library( rts SHARED IMPORTED )
|
||||||
FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/libHSrts*_thr-*.${OS_LIB_EXT})
|
FILE(GLOB RTSLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/deps/libHSrts*_thr-*.${OS_LIB_EXT})
|
||||||
set_target_properties( rts PROPERTIES IMPORTED_LOCATION ${RTSLIB})
|
set_target_properties( rts PROPERTIES IMPORTED_LOCATION ${RTSLIB})
|
||||||
|
|
||||||
target_link_libraries(app-lib rts simplex)
|
target_link_libraries(app-lib rts simplex)
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ import kotlinx.coroutines.flow.*
|
|||||||
|
|
||||||
data class SettingsViewState(
|
data class SettingsViewState(
|
||||||
val userPickerState: MutableStateFlow<AnimatedViewState>,
|
val userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||||
val scaffoldState: ScaffoldState,
|
val scaffoldState: ScaffoldState
|
||||||
val switchingUsersAndHosts: MutableState<Boolean>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -102,11 +101,8 @@ fun MainScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
var onboarding by remember { mutableStateOf(chatModel.controller.appPrefs.onboardingStage.get()) }
|
val onboarding by remember { chatModel.controller.appPrefs.onboardingStage.state }
|
||||||
LaunchedEffect(Unit) {
|
val localUserCreated = chatModel.localUserCreated.value
|
||||||
snapshotFlow { chatModel.controller.appPrefs.onboardingStage.state.value }.distinctUntilChanged().collect { onboarding = it }
|
|
||||||
}
|
|
||||||
val userCreated = chatModel.userCreated.value
|
|
||||||
var showInitializationView by remember { mutableStateOf(false) }
|
var showInitializationView by remember { mutableStateOf(false) }
|
||||||
when {
|
when {
|
||||||
chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView()
|
chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView()
|
||||||
@@ -115,14 +111,18 @@ fun MainScreen() {
|
|||||||
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
|
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
remember { chatModel.chatDbEncrypted }.value == null || userCreated == null -> SplashView()
|
remember { chatModel.chatDbEncrypted }.value == null || localUserCreated == null -> SplashView()
|
||||||
onboarding == OnboardingStage.OnboardingComplete && userCreated -> {
|
onboarding == OnboardingStage.OnboardingComplete -> {
|
||||||
Box {
|
Box {
|
||||||
showAdvertiseLAAlert = true
|
showAdvertiseLAAlert = true
|
||||||
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(if (chatModel.desktopNoUserNoRemote()) AnimatedViewState.VISIBLE else AnimatedViewState.GONE)) }
|
||||||
|
KeyChangeEffect(chatModel.desktopNoUserNoRemote) {
|
||||||
|
if (chatModel.desktopNoUserNoRemote() && !ModalManager.start.hasModalsOpen()) {
|
||||||
|
userPickerState.value = AnimatedViewState.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
val scaffoldState = rememberScaffoldState()
|
val scaffoldState = rememberScaffoldState()
|
||||||
val switchingUsersAndHosts = rememberSaveable { mutableStateOf(false) }
|
val settingsState = remember { SettingsViewState(userPickerState, scaffoldState) }
|
||||||
val settingsState = remember { SettingsViewState(userPickerState, scaffoldState, switchingUsersAndHosts) }
|
|
||||||
if (appPlatform.isAndroid) {
|
if (appPlatform.isAndroid) {
|
||||||
AndroidScreen(settingsState)
|
AndroidScreen(settingsState)
|
||||||
} else {
|
} else {
|
||||||
@@ -137,12 +137,14 @@ fun MainScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {}
|
onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {}
|
||||||
|
onboarding == OnboardingStage.LinkAMobile -> LinkAMobile()
|
||||||
onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel)
|
onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel)
|
||||||
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel, null)
|
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel, null)
|
||||||
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
|
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
|
||||||
}
|
}
|
||||||
if (appPlatform.isAndroid) {
|
if (appPlatform.isAndroid) {
|
||||||
ModalManager.fullscreen.showInView()
|
ModalManager.fullscreen.showInView()
|
||||||
|
SwitchingUsersView()
|
||||||
}
|
}
|
||||||
|
|
||||||
val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } }
|
val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } }
|
||||||
@@ -262,7 +264,7 @@ fun CenterPartOfScreen() {
|
|||||||
.background(MaterialTheme.colors.background),
|
.background(MaterialTheme.colors.background),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(stringResource(MR.strings.no_selected_chat))
|
Text(stringResource(if (chatModel.desktopNoUserNoRemote) MR.strings.no_connected_mobile else MR.strings.no_selected_chat))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ModalManager.center.showInView()
|
ModalManager.center.showInView()
|
||||||
@@ -286,6 +288,7 @@ fun DesktopScreen(settingsState: SettingsViewState) {
|
|||||||
}
|
}
|
||||||
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) {
|
Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) {
|
||||||
ModalManager.start.showInView()
|
ModalManager.start.showInView()
|
||||||
|
SwitchingUsersView()
|
||||||
}
|
}
|
||||||
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) {
|
Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) {
|
||||||
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
|
Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) {
|
||||||
@@ -298,7 +301,7 @@ fun DesktopScreen(settingsState: SettingsViewState) {
|
|||||||
EndPartOfScreen()
|
EndPartOfScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val (userPickerState, scaffoldState, switchingUsersAndHosts ) = settingsState
|
val (userPickerState, scaffoldState ) = settingsState
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
if (scaffoldState.drawerState.isOpen) {
|
if (scaffoldState.drawerState.isOpen) {
|
||||||
Box(
|
Box(
|
||||||
@@ -312,7 +315,7 @@ fun DesktopScreen(settingsState: SettingsViewState) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH))
|
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH))
|
||||||
UserPicker(chatModel, userPickerState, switchingUsersAndHosts) {
|
UserPicker(chatModel, userPickerState) {
|
||||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||||
userPickerState.value = AnimatedViewState.GONE
|
userPickerState.value = AnimatedViewState.GONE
|
||||||
}
|
}
|
||||||
@@ -335,3 +338,26 @@ fun InitializationView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SwitchingUsersView() {
|
||||||
|
if (remember { chatModel.switchingUsersAndHosts }.value) {
|
||||||
|
Box(
|
||||||
|
Modifier.fillMaxSize().clickable(enabled = false, onClick = {}),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
ProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProgressIndicator() {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = 2.dp)
|
||||||
|
.size(30.dp),
|
||||||
|
color = MaterialTheme.colors.secondary,
|
||||||
|
strokeWidth = 2.5.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+31
-3
@@ -2,7 +2,7 @@ package chat.simplex.common.model
|
|||||||
|
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
@@ -43,7 +43,7 @@ object ChatModel {
|
|||||||
val setDeliveryReceipts = mutableStateOf(false)
|
val setDeliveryReceipts = mutableStateOf(false)
|
||||||
val currentUser = mutableStateOf<User?>(null)
|
val currentUser = mutableStateOf<User?>(null)
|
||||||
val users = mutableStateListOf<UserInfo>()
|
val users = mutableStateListOf<UserInfo>()
|
||||||
val userCreated = mutableStateOf<Boolean?>(null)
|
val localUserCreated = mutableStateOf<Boolean?>(null)
|
||||||
val chatRunning = mutableStateOf<Boolean?>(null)
|
val chatRunning = mutableStateOf<Boolean?>(null)
|
||||||
val chatDbChanged = mutableStateOf<Boolean>(false)
|
val chatDbChanged = mutableStateOf<Boolean>(false)
|
||||||
val chatDbEncrypted = mutableStateOf<Boolean?>(false)
|
val chatDbEncrypted = mutableStateOf<Boolean?>(false)
|
||||||
@@ -51,6 +51,7 @@ object ChatModel {
|
|||||||
val chats = mutableStateListOf<Chat>()
|
val chats = mutableStateListOf<Chat>()
|
||||||
// map of connections network statuses, key is agent connection id
|
// map of connections network statuses, key is agent connection id
|
||||||
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
||||||
|
val switchingUsersAndHosts = mutableStateOf(false)
|
||||||
|
|
||||||
// current chat
|
// current chat
|
||||||
val chatId = mutableStateOf<String?>(null)
|
val chatId = mutableStateOf<String?>(null)
|
||||||
@@ -108,6 +109,9 @@ object ChatModel {
|
|||||||
|
|
||||||
var updatingChatsMutex: Mutex = Mutex()
|
var updatingChatsMutex: Mutex = Mutex()
|
||||||
|
|
||||||
|
val desktopNoUserNoRemote: Boolean @Composable get() = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null
|
||||||
|
fun desktopNoUserNoRemote(): Boolean = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null
|
||||||
|
|
||||||
// remote controller
|
// remote controller
|
||||||
val remoteHosts = mutableStateListOf<RemoteHostInfo>()
|
val remoteHosts = mutableStateListOf<RemoteHostInfo>()
|
||||||
val currentRemoteHost = mutableStateOf<RemoteHostInfo?>(null)
|
val currentRemoteHost = mutableStateOf<RemoteHostInfo?>(null)
|
||||||
@@ -222,8 +226,23 @@ object ChatModel {
|
|||||||
val chat: Chat
|
val chat: Chat
|
||||||
if (i >= 0) {
|
if (i >= 0) {
|
||||||
chat = chats[i]
|
chat = chats[i]
|
||||||
|
val newPreviewItem = when (cInfo) {
|
||||||
|
is ChatInfo.Group -> {
|
||||||
|
val currentPreviewItem = chat.chatItems.firstOrNull()
|
||||||
|
if (currentPreviewItem != null) {
|
||||||
|
if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) {
|
||||||
|
cItem
|
||||||
|
} else {
|
||||||
|
currentPreviewItem
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> cItem
|
||||||
|
}
|
||||||
chats[i] = chat.copy(
|
chats[i] = chat.copy(
|
||||||
chatItems = arrayListOf(cItem),
|
chatItems = arrayListOf(newPreviewItem),
|
||||||
chatStats =
|
chatStats =
|
||||||
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
|
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
|
||||||
val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId
|
val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId
|
||||||
@@ -605,6 +624,7 @@ object ChatModel {
|
|||||||
terminalItems.add(item)
|
terminalItems.add(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val connectedToRemote: Boolean @Composable get() = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true
|
||||||
fun connectedToRemote(): Boolean = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true
|
fun connectedToRemote(): Boolean = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2945,6 +2965,14 @@ sealed class RemoteCtrlSessionState {
|
|||||||
@Serializable @SerialName("connected") data class Connected(val sessionCode: String): RemoteCtrlSessionState()
|
@Serializable @SerialName("connected") data class Connected(val sessionCode: String): RemoteCtrlSessionState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class RemoteCtrlStopReason {
|
||||||
|
@Serializable @SerialName("discoveryFailed") class DiscoveryFailed(val chatError: ChatError): RemoteCtrlStopReason()
|
||||||
|
@Serializable @SerialName("connectionFailed") class ConnectionFailed(val chatError: ChatError): RemoteCtrlStopReason()
|
||||||
|
@Serializable @SerialName("setupFailed") class SetupFailed(val chatError: ChatError): RemoteCtrlStopReason()
|
||||||
|
@Serializable @SerialName("disconnected") object Disconnected: RemoteCtrlStopReason()
|
||||||
|
}
|
||||||
|
|
||||||
sealed class UIRemoteCtrlSessionState {
|
sealed class UIRemoteCtrlSessionState {
|
||||||
object Starting: UIRemoteCtrlSessionState()
|
object Starting: UIRemoteCtrlSessionState()
|
||||||
object Searching: UIRemoteCtrlSessionState()
|
object Searching: UIRemoteCtrlSessionState()
|
||||||
|
|||||||
+93
-39
@@ -173,6 +173,8 @@ class AppPreferences {
|
|||||||
val connectRemoteViaMulticastAuto = mkBoolPreference(SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO, true)
|
val connectRemoteViaMulticastAuto = mkBoolPreference(SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO, true)
|
||||||
val offerRemoteMulticast = mkBoolPreference(SHARED_PREFS_OFFER_REMOTE_MULTICAST, true)
|
val offerRemoteMulticast = mkBoolPreference(SHARED_PREFS_OFFER_REMOTE_MULTICAST, true)
|
||||||
|
|
||||||
|
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
|
||||||
|
|
||||||
private fun mkIntPreference(prefName: String, default: Int) =
|
private fun mkIntPreference(prefName: String, default: Int) =
|
||||||
SharedPreference(
|
SharedPreference(
|
||||||
get = fun() = settings.getInt(prefName, default),
|
get = fun() = settings.getInt(prefName, default),
|
||||||
@@ -317,6 +319,7 @@ class AppPreferences {
|
|||||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST = "ConnectRemoteViaMulticast"
|
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST = "ConnectRemoteViaMulticast"
|
||||||
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
|
||||||
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
|
||||||
|
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,7 +362,7 @@ object ChatController {
|
|||||||
chatModel.users.addAll(users)
|
chatModel.users.addAll(users)
|
||||||
if (justStarted) {
|
if (justStarted) {
|
||||||
chatModel.currentUser.value = user
|
chatModel.currentUser.value = user
|
||||||
chatModel.userCreated.value = true
|
chatModel.localUserCreated.value = true
|
||||||
getUserChatData(null)
|
getUserChatData(null)
|
||||||
appPrefs.chatLastStart.set(Clock.System.now())
|
appPrefs.chatLastStart.set(Clock.System.now())
|
||||||
chatModel.chatRunning.value = true
|
chatModel.chatRunning.value = true
|
||||||
@@ -379,6 +382,31 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun startChatWithoutUser() {
|
||||||
|
Log.d(TAG, "user: null")
|
||||||
|
try {
|
||||||
|
if (chatModel.chatRunning.value == true) return
|
||||||
|
apiSetTempFolder(coreTmpDir.absolutePath)
|
||||||
|
apiSetFilesFolder(appFilesDir.absolutePath)
|
||||||
|
if (appPlatform.isDesktop) {
|
||||||
|
apiSetRemoteHostsFolder(remoteHostsDir.absolutePath)
|
||||||
|
}
|
||||||
|
apiSetXFTPConfig(getXFTPCfg())
|
||||||
|
apiSetEncryptLocalFiles(appPrefs.privacyEncryptLocalFiles.get())
|
||||||
|
chatModel.users.clear()
|
||||||
|
chatModel.currentUser.value = null
|
||||||
|
chatModel.localUserCreated.value = false
|
||||||
|
appPrefs.chatLastStart.set(Clock.System.now())
|
||||||
|
chatModel.chatRunning.value = true
|
||||||
|
startReceiver()
|
||||||
|
setLocalDeviceName(appPrefs.deviceNameForRemoteAccess.get()!!)
|
||||||
|
Log.d(TAG, "startChat: started without user")
|
||||||
|
} catch (e: Error) {
|
||||||
|
Log.e(TAG, "failed starting chat without user $e")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun changeActiveUser(rhId: Long?, toUserId: Long, viewPwd: String?) {
|
suspend fun changeActiveUser(rhId: Long?, toUserId: Long, viewPwd: String?) {
|
||||||
try {
|
try {
|
||||||
changeActiveUser_(rhId, toUserId, viewPwd)
|
changeActiveUser_(rhId, toUserId, viewPwd)
|
||||||
@@ -402,8 +430,9 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getUserChatData(rhId: Long?) {
|
suspend fun getUserChatData(rhId: Long?) {
|
||||||
chatModel.userAddress.value = apiGetUserAddress(rhId)
|
val hasUser = chatModel.currentUser.value != null
|
||||||
chatModel.chatItemTTL.value = getChatItemTTL(rhId)
|
chatModel.userAddress.value = if (hasUser) apiGetUserAddress(rhId) else null
|
||||||
|
chatModel.chatItemTTL.value = if (hasUser) getChatItemTTL(rhId) else ChatItemTTL.None
|
||||||
updatingChatsMutex.withLock {
|
updatingChatsMutex.withLock {
|
||||||
val chats = apiGetChats(rhId)
|
val chats = apiGetChats(rhId)
|
||||||
chatModel.updateChats(chats)
|
chatModel.updateChats(chats)
|
||||||
@@ -472,7 +501,9 @@ object ChatController {
|
|||||||
val r = sendCmd(rh, CC.ShowActiveUser())
|
val r = sendCmd(rh, CC.ShowActiveUser())
|
||||||
if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh)
|
if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh)
|
||||||
Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}")
|
Log.d(TAG, "apiGetActiveUser: ${r.responseType} ${r.details}")
|
||||||
chatModel.userCreated.value = false
|
if (rh == null) {
|
||||||
|
chatModel.localUserCreated.value = false
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,20 +922,21 @@ object ChatController {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiConnect(rh: Long?, incognito: Boolean, connReq: String): Boolean {
|
suspend fun apiConnect(rh: Long?, incognito: Boolean, connReq: String): PendingContactConnection? {
|
||||||
val userId = chatModel.currentUser.value?.userId ?: run {
|
val userId = chatModel.currentUser.value?.userId ?: run {
|
||||||
Log.e(TAG, "apiConnect: no current user")
|
Log.e(TAG, "apiConnect: no current user")
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
val r = sendCmd(rh, CC.APIConnect(userId, incognito, connReq))
|
val r = sendCmd(rh, CC.APIConnect(userId, incognito, connReq))
|
||||||
when {
|
when {
|
||||||
r is CR.SentConfirmation || r is CR.SentInvitation -> return true
|
r is CR.SentConfirmation -> return r.connection
|
||||||
|
r is CR.SentInvitation -> return r.connection
|
||||||
r is CR.ContactAlreadyExists -> {
|
r is CR.ContactAlreadyExists -> {
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
generalGetString(MR.strings.contact_already_exists),
|
generalGetString(MR.strings.contact_already_exists),
|
||||||
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), r.contact.displayName)
|
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), r.contact.displayName)
|
||||||
)
|
)
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat
|
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat
|
||||||
&& r.chatError.errorType is ChatErrorType.InvalidConnReq -> {
|
&& r.chatError.errorType is ChatErrorType.InvalidConnReq -> {
|
||||||
@@ -912,7 +944,7 @@ object ChatController {
|
|||||||
generalGetString(MR.strings.invalid_connection_link),
|
generalGetString(MR.strings.invalid_connection_link),
|
||||||
generalGetString(MR.strings.please_check_correct_link_and_maybe_ask_for_a_new_one)
|
generalGetString(MR.strings.please_check_correct_link_and_maybe_ask_for_a_new_one)
|
||||||
)
|
)
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
|
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
|
||||||
&& r.chatError.agentError is AgentErrorType.SMP
|
&& r.chatError.agentError is AgentErrorType.SMP
|
||||||
@@ -921,13 +953,13 @@ object ChatController {
|
|||||||
generalGetString(MR.strings.connection_error_auth),
|
generalGetString(MR.strings.connection_error_auth),
|
||||||
generalGetString(MR.strings.connection_error_auth_desc)
|
generalGetString(MR.strings.connection_error_auth_desc)
|
||||||
)
|
)
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (!(networkErrorAlert(r))) {
|
if (!(networkErrorAlert(r))) {
|
||||||
apiErrorAlert("apiConnect", generalGetString(MR.strings.connection_error), r)
|
apiErrorAlert("apiConnect", generalGetString(MR.strings.connection_error), r)
|
||||||
}
|
}
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1394,9 +1426,9 @@ object ChatController {
|
|||||||
chatModel.remoteHosts.addAll(hosts)
|
chatModel.remoteHosts.addAll(hosts)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true): Triple<RemoteHostInfo?, String, String>? {
|
suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true, address: RemoteCtrlAddress?, port: Int?): CR.RemoteHostStarted? {
|
||||||
val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast))
|
val r = sendCmd(null, CC.StartRemoteHost(rhId, multicast, address, port))
|
||||||
if (r is CR.RemoteHostStarted) return Triple(r.remoteHost_, r.invitation, r.ctrlPort)
|
if (r is CR.RemoteHostStarted) return r
|
||||||
apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r)
|
apiErrorAlert("startRemoteHost", generalGetString(MR.strings.error_alert_title), r)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -1526,16 +1558,6 @@ object ChatController {
|
|||||||
fun active(user: UserLike): Boolean = activeUser(rhId, user)
|
fun active(user: UserLike): Boolean = activeUser(rhId, user)
|
||||||
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
|
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
|
||||||
when (r) {
|
when (r) {
|
||||||
is CR.NewContactConnection -> {
|
|
||||||
if (active(r.user)) {
|
|
||||||
chatModel.updateContactConnection(rhId, r.connection)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is CR.ContactConnectionDeleted -> {
|
|
||||||
if (active(r.user)) {
|
|
||||||
chatModel.removeChat(rhId, r.connection.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is CR.ContactDeletedByContact -> {
|
is CR.ContactDeletedByContact -> {
|
||||||
if (active(r.user) && r.contact.directOrUsed) {
|
if (active(r.user) && r.contact.directOrUsed) {
|
||||||
chatModel.updateContact(rhId, r.contact)
|
chatModel.updateContact(rhId, r.contact)
|
||||||
@@ -1996,7 +2018,7 @@ object ChatController {
|
|||||||
chatModel.setContactNetworkStatus(contact, NetworkStatus.Error(err))
|
chatModel.setContactNetworkStatus(contact, NetworkStatus.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun switchUIRemoteHost(rhId: Long?) {
|
suspend fun switchUIRemoteHost(rhId: Long?) = showProgressIfNeeded {
|
||||||
// TODO lock the switch so that two switches can't run concurrently?
|
// TODO lock the switch so that two switches can't run concurrently?
|
||||||
chatModel.chatId.value = null
|
chatModel.chatId.value = null
|
||||||
ModalManager.center.closeModals()
|
ModalManager.center.closeModals()
|
||||||
@@ -2009,7 +2031,10 @@ object ChatController {
|
|||||||
chatModel.users.clear()
|
chatModel.users.clear()
|
||||||
chatModel.users.addAll(users)
|
chatModel.users.addAll(users)
|
||||||
chatModel.currentUser.value = user
|
chatModel.currentUser.value = user
|
||||||
chatModel.userCreated.value = true
|
if (user == null) {
|
||||||
|
chatModel.chatItems.clear()
|
||||||
|
chatModel.chats.clear()
|
||||||
|
}
|
||||||
val statuses = apiGetNetworkStatuses(rhId)
|
val statuses = apiGetNetworkStatuses(rhId)
|
||||||
if (statuses != null) {
|
if (statuses != null) {
|
||||||
chatModel.networkStatuses.clear()
|
chatModel.networkStatuses.clear()
|
||||||
@@ -2019,6 +2044,23 @@ object ChatController {
|
|||||||
getUserChatData(rhId)
|
getUserChatData(rhId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun showProgressIfNeeded(block: suspend () -> Unit) {
|
||||||
|
val job = withBGApi {
|
||||||
|
try {
|
||||||
|
delay(500)
|
||||||
|
chatModel.switchingUsersAndHosts.value = true
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
chatModel.switchingUsersAndHosts.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
job.cancel()
|
||||||
|
chatModel.switchingUsersAndHosts.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getXFTPCfg(): XFTPFileConfig {
|
fun getXFTPCfg(): XFTPFileConfig {
|
||||||
return XFTPFileConfig(minFileSize = 0)
|
return XFTPFileConfig(minFileSize = 0)
|
||||||
}
|
}
|
||||||
@@ -2206,7 +2248,7 @@ sealed class CC {
|
|||||||
// Remote control
|
// Remote control
|
||||||
class SetLocalDeviceName(val displayName: String): CC()
|
class SetLocalDeviceName(val displayName: String): CC()
|
||||||
class ListRemoteHosts(): CC()
|
class ListRemoteHosts(): CC()
|
||||||
class StartRemoteHost(val remoteHostId: Long?, val multicast: Boolean): CC()
|
class StartRemoteHost(val remoteHostId: Long?, val multicast: Boolean, val address: RemoteCtrlAddress?, val port: Int?): CC()
|
||||||
class SwitchRemoteHost (val remoteHostId: Long?): CC()
|
class SwitchRemoteHost (val remoteHostId: Long?): CC()
|
||||||
class StopRemoteHost(val remoteHostKey: Long?): CC()
|
class StopRemoteHost(val remoteHostKey: Long?): CC()
|
||||||
class DeleteRemoteHost(val remoteHostId: Long): CC()
|
class DeleteRemoteHost(val remoteHostId: Long): CC()
|
||||||
@@ -2342,7 +2384,7 @@ sealed class CC {
|
|||||||
is CancelFile -> "/fcancel $fileId"
|
is CancelFile -> "/fcancel $fileId"
|
||||||
is SetLocalDeviceName -> "/set device name $displayName"
|
is SetLocalDeviceName -> "/set device name $displayName"
|
||||||
is ListRemoteHosts -> "/list remote hosts"
|
is ListRemoteHosts -> "/list remote hosts"
|
||||||
is StartRemoteHost -> "/start remote host " + if (remoteHostId == null) "new" else "$remoteHostId multicast=${onOff(multicast)}"
|
is StartRemoteHost -> "/start remote host " + (if (remoteHostId == null) "new" else "$remoteHostId multicast=${onOff(multicast)}") + (if (address != null) " addr=${address.address} iface=${address.`interface`}" else "") + (if (port != null) " port=$port" else "")
|
||||||
is SwitchRemoteHost -> "/switch remote host " + if (remoteHostId == null) "local" else "$remoteHostId"
|
is SwitchRemoteHost -> "/switch remote host " + if (remoteHostId == null) "local" else "$remoteHostId"
|
||||||
is StopRemoteHost -> "/stop remote host " + if (remoteHostKey == null) "new" else "$remoteHostKey"
|
is StopRemoteHost -> "/stop remote host " + if (remoteHostKey == null) "new" else "$remoteHostKey"
|
||||||
is DeleteRemoteHost -> "/delete remote host $remoteHostId"
|
is DeleteRemoteHost -> "/delete remote host $remoteHostId"
|
||||||
@@ -3564,6 +3606,8 @@ data class RemoteHostInfo(
|
|||||||
val remoteHostId: Long,
|
val remoteHostId: Long,
|
||||||
val hostDeviceName: String,
|
val hostDeviceName: String,
|
||||||
val storePath: String,
|
val storePath: String,
|
||||||
|
val bindAddress_: RemoteCtrlAddress?,
|
||||||
|
val bindPort_: Int?,
|
||||||
val sessionState: RemoteHostSessionState?
|
val sessionState: RemoteHostSessionState?
|
||||||
) {
|
) {
|
||||||
val activeHost: Boolean
|
val activeHost: Boolean
|
||||||
@@ -3572,6 +3616,12 @@ data class RemoteHostInfo(
|
|||||||
fun activeHost(): Boolean = chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId
|
fun activeHost(): Boolean = chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RemoteCtrlAddress(
|
||||||
|
val address: String,
|
||||||
|
val `interface`: String
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed class RemoteHostSessionState {
|
sealed class RemoteHostSessionState {
|
||||||
@Serializable @SerialName("starting") object Starting: RemoteHostSessionState()
|
@Serializable @SerialName("starting") object Starting: RemoteHostSessionState()
|
||||||
@@ -3581,6 +3631,13 @@ sealed class RemoteHostSessionState {
|
|||||||
@Serializable @SerialName("connected") data class Connected(val sessionCode: String): RemoteHostSessionState()
|
@Serializable @SerialName("connected") data class Connected(val sessionCode: String): RemoteHostSessionState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class RemoteHostStopReason {
|
||||||
|
@Serializable @SerialName("connectionFailed") data class ConnectionFailed(val chatError: ChatError): RemoteHostStopReason()
|
||||||
|
@Serializable @SerialName("crashed") data class Crashed(val chatError: ChatError): RemoteHostStopReason()
|
||||||
|
@Serializable @SerialName("disconnected") object Disconnected: RemoteHostStopReason()
|
||||||
|
}
|
||||||
|
|
||||||
val json = Json {
|
val json = Json {
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
@@ -3700,8 +3757,8 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR()
|
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR()
|
||||||
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
|
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
|
||||||
@Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR()
|
@Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR()
|
||||||
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef): CR()
|
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||||
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef): CR()
|
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||||
@Serializable @SerialName("sentInvitationToContact") class SentInvitationToContact(val user: UserRef, val contact: Contact, val customUserProfile: Profile?): CR()
|
@Serializable @SerialName("sentInvitationToContact") class SentInvitationToContact(val user: UserRef, val contact: Contact, val customUserProfile: Profile?): CR()
|
||||||
@Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR()
|
@Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR()
|
||||||
@Serializable @SerialName("contactRequestAlreadyAccepted") class ContactRequestAlreadyAccepted(val user: UserRef, val contact: Contact): CR()
|
@Serializable @SerialName("contactRequestAlreadyAccepted") class ContactRequestAlreadyAccepted(val user: UserRef, val contact: Contact): CR()
|
||||||
@@ -3795,16 +3852,15 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("callAnswer") class CallAnswer(val user: UserRef, val contact: Contact, val answer: WebRTCSession): CR()
|
@Serializable @SerialName("callAnswer") class CallAnswer(val user: UserRef, val contact: Contact, val answer: WebRTCSession): CR()
|
||||||
@Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR()
|
@Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR()
|
||||||
@Serializable @SerialName("callEnded") class CallEnded(val user: UserRef, val contact: Contact): CR()
|
@Serializable @SerialName("callEnded") class CallEnded(val user: UserRef, val contact: Contact): CR()
|
||||||
@Serializable @SerialName("newContactConnection") class NewContactConnection(val user: UserRef, val connection: PendingContactConnection): CR()
|
|
||||||
@Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: UserRef, val connection: PendingContactConnection): CR()
|
@Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: UserRef, val connection: PendingContactConnection): CR()
|
||||||
// remote events (desktop)
|
// remote events (desktop)
|
||||||
@Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR()
|
@Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR()
|
||||||
@Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR()
|
@Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR()
|
||||||
@Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHost_: RemoteHostInfo?, val invitation: String, val ctrlPort: String): CR()
|
@Serializable @SerialName("remoteHostStarted") class RemoteHostStarted(val remoteHost_: RemoteHostInfo?, val invitation: String, val localAddrs: List<RemoteCtrlAddress>, val ctrlPort: String): CR()
|
||||||
@Serializable @SerialName("remoteHostSessionCode") class RemoteHostSessionCode(val remoteHost_: RemoteHostInfo?, val sessionCode: String): CR()
|
@Serializable @SerialName("remoteHostSessionCode") class RemoteHostSessionCode(val remoteHost_: RemoteHostInfo?, val sessionCode: String): CR()
|
||||||
@Serializable @SerialName("newRemoteHost") class NewRemoteHost(val remoteHost: RemoteHostInfo): CR()
|
@Serializable @SerialName("newRemoteHost") class NewRemoteHost(val remoteHost: RemoteHostInfo): CR()
|
||||||
@Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHost: RemoteHostInfo): CR()
|
@Serializable @SerialName("remoteHostConnected") class RemoteHostConnected(val remoteHost: RemoteHostInfo): CR()
|
||||||
@Serializable @SerialName("remoteHostStopped") class RemoteHostStopped(val remoteHostId_: Long?): CR()
|
@Serializable @SerialName("remoteHostStopped") class RemoteHostStopped(val remoteHostId_: Long?, val rhsState: RemoteHostSessionState, val rhStopReason: RemoteHostStopReason): CR()
|
||||||
@Serializable @SerialName("remoteFileStored") class RemoteFileStored(val remoteHostId: Long, val remoteFileSource: CryptoFile): CR()
|
@Serializable @SerialName("remoteFileStored") class RemoteFileStored(val remoteHostId: Long, val remoteFileSource: CryptoFile): CR()
|
||||||
// remote events (mobile)
|
// remote events (mobile)
|
||||||
@Serializable @SerialName("remoteCtrlList") class RemoteCtrlList(val remoteCtrls: List<RemoteCtrlInfo>): CR()
|
@Serializable @SerialName("remoteCtrlList") class RemoteCtrlList(val remoteCtrls: List<RemoteCtrlInfo>): CR()
|
||||||
@@ -3812,7 +3868,7 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("remoteCtrlConnecting") class RemoteCtrlConnecting(val remoteCtrl_: RemoteCtrlInfo?, val ctrlAppInfo: CtrlAppInfo, val appVersion: String): CR()
|
@Serializable @SerialName("remoteCtrlConnecting") class RemoteCtrlConnecting(val remoteCtrl_: RemoteCtrlInfo?, val ctrlAppInfo: CtrlAppInfo, val appVersion: String): CR()
|
||||||
@Serializable @SerialName("remoteCtrlSessionCode") class RemoteCtrlSessionCode(val remoteCtrl_: RemoteCtrlInfo?, val sessionCode: String): CR()
|
@Serializable @SerialName("remoteCtrlSessionCode") class RemoteCtrlSessionCode(val remoteCtrl_: RemoteCtrlInfo?, val sessionCode: String): CR()
|
||||||
@Serializable @SerialName("remoteCtrlConnected") class RemoteCtrlConnected(val remoteCtrl: RemoteCtrlInfo): CR()
|
@Serializable @SerialName("remoteCtrlConnected") class RemoteCtrlConnected(val remoteCtrl: RemoteCtrlInfo): CR()
|
||||||
@Serializable @SerialName("remoteCtrlStopped") class RemoteCtrlStopped(): CR()
|
@Serializable @SerialName("remoteCtrlStopped") class RemoteCtrlStopped(val rcsState: RemoteCtrlSessionState, val rcStopReason: RemoteCtrlStopReason): CR()
|
||||||
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List<UpMigration>, val agentMigrations: List<UpMigration>): CR()
|
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List<UpMigration>, val agentMigrations: List<UpMigration>): CR()
|
||||||
@Serializable @SerialName("cmdOk") class CmdOk(val user: UserRef?): CR()
|
@Serializable @SerialName("cmdOk") class CmdOk(val user: UserRef?): CR()
|
||||||
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR()
|
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR()
|
||||||
@@ -3944,7 +4000,6 @@ sealed class CR {
|
|||||||
is CallAnswer -> "callAnswer"
|
is CallAnswer -> "callAnswer"
|
||||||
is CallExtraInfo -> "callExtraInfo"
|
is CallExtraInfo -> "callExtraInfo"
|
||||||
is CallEnded -> "callEnded"
|
is CallEnded -> "callEnded"
|
||||||
is NewContactConnection -> "newContactConnection"
|
|
||||||
is ContactConnectionDeleted -> "contactConnectionDeleted"
|
is ContactConnectionDeleted -> "contactConnectionDeleted"
|
||||||
is RemoteHostList -> "remoteHostList"
|
is RemoteHostList -> "remoteHostList"
|
||||||
is CurrentRemoteHost -> "currentRemoteHost"
|
is CurrentRemoteHost -> "currentRemoteHost"
|
||||||
@@ -3999,11 +4054,11 @@ sealed class CR {
|
|||||||
is ContactCode -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode")
|
is ContactCode -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode")
|
||||||
is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode")
|
is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode")
|
||||||
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
|
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
|
||||||
is Invitation -> withUser(user, connReqInvitation)
|
is Invitation -> withUser(user, "connReqInvitation: $connReqInvitation\nconnection: $connection")
|
||||||
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
|
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
|
||||||
is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan))
|
is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan))
|
||||||
is SentConfirmation -> withUser(user, noDetails())
|
is SentConfirmation -> withUser(user, json.encodeToString(connection))
|
||||||
is SentInvitation -> withUser(user, noDetails())
|
is SentInvitation -> withUser(user, json.encodeToString(connection))
|
||||||
is SentInvitationToContact -> withUser(user, json.encodeToString(contact))
|
is SentInvitationToContact -> withUser(user, json.encodeToString(contact))
|
||||||
is ContactAlreadyExists -> withUser(user, json.encodeToString(contact))
|
is ContactAlreadyExists -> withUser(user, json.encodeToString(contact))
|
||||||
is ContactRequestAlreadyAccepted -> withUser(user, json.encodeToString(contact))
|
is ContactRequestAlreadyAccepted -> withUser(user, json.encodeToString(contact))
|
||||||
@@ -4091,7 +4146,6 @@ sealed class CR {
|
|||||||
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
|
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
|
||||||
is CallExtraInfo -> withUser(user, "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}")
|
is CallExtraInfo -> withUser(user, "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}")
|
||||||
is CallEnded -> withUser(user, "contact: ${contact.id}")
|
is CallEnded -> withUser(user, "contact: ${contact.id}")
|
||||||
is NewContactConnection -> withUser(user, json.encodeToString(connection))
|
|
||||||
is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection))
|
is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection))
|
||||||
// remote events (mobile)
|
// remote events (mobile)
|
||||||
is RemoteHostList -> json.encodeToString(remoteHosts)
|
is RemoteHostList -> json.encodeToString(remoteHosts)
|
||||||
|
|||||||
+13
-1
@@ -55,10 +55,22 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
|||||||
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
||||||
val user = chatController.apiGetActiveUser(null)
|
val user = chatController.apiGetActiveUser(null)
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
|
||||||
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||||
chatModel.currentUser.value = null
|
chatModel.currentUser.value = null
|
||||||
chatModel.users.clear()
|
chatModel.users.clear()
|
||||||
|
if (appPlatform.isDesktop) {
|
||||||
|
/**
|
||||||
|
* Setting it here to null because otherwise the screen will flash in [MainScreen] after the first start
|
||||||
|
* because of default value of [OnboardingStage.OnboardingComplete]
|
||||||
|
* */
|
||||||
|
chatModel.localUserCreated.value = null
|
||||||
|
if (chatController.listRemoteHosts()?.isEmpty() == true) {
|
||||||
|
chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||||
|
}
|
||||||
|
chatController.startChatWithoutUser()
|
||||||
|
} else {
|
||||||
|
chatController.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val savedOnboardingStage = appPreferences.onboardingStage.get()
|
val savedOnboardingStage = appPreferences.onboardingStage.get()
|
||||||
appPreferences.onboardingStage.set(if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
|
appPreferences.onboardingStage.set(if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
|
||||||
|
|||||||
+6
-2
@@ -59,7 +59,9 @@ abstract class NtfManager {
|
|||||||
awaitChatStartedIfNeeded(chatModel)
|
awaitChatStartedIfNeeded(chatModel)
|
||||||
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
||||||
// TODO include remote host ID in desktop notifications?
|
// TODO include remote host ID in desktop notifications?
|
||||||
chatModel.controller.changeActiveUser(null, userId, null)
|
chatModel.controller.showProgressIfNeeded {
|
||||||
|
chatModel.controller.changeActiveUser(null, userId, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val cInfo = chatModel.getChat(chatId)?.chatInfo
|
val cInfo = chatModel.getChat(chatId)?.chatInfo
|
||||||
chatModel.clearOverlays.value = true
|
chatModel.clearOverlays.value = true
|
||||||
@@ -72,7 +74,9 @@ abstract class NtfManager {
|
|||||||
awaitChatStartedIfNeeded(chatModel)
|
awaitChatStartedIfNeeded(chatModel)
|
||||||
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
||||||
// TODO include remote host ID in desktop notifications?
|
// TODO include remote host ID in desktop notifications?
|
||||||
chatModel.controller.changeActiveUser(null, userId, null)
|
chatModel.controller.showProgressIfNeeded {
|
||||||
|
chatModel.controller.changeActiveUser(null, userId, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
chatModel.chatId.value = null
|
chatModel.chatId.value = null
|
||||||
chatModel.clearOverlays.value = true
|
chatModel.clearOverlays.value = true
|
||||||
|
|||||||
+20
-3
@@ -21,8 +21,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.*
|
import androidx.compose.ui.text.style.*
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.common.model.ChatModel
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.model.Profile
|
import chat.simplex.common.model.ChatModel.controller
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
@@ -76,7 +76,13 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
|
|||||||
disabled = !canCreateProfile(displayName.value),
|
disabled = !canCreateProfile(displayName.value),
|
||||||
textColor = MaterialTheme.colors.primary,
|
textColor = MaterialTheme.colors.primary,
|
||||||
iconColor = MaterialTheme.colors.primary,
|
iconColor = MaterialTheme.colors.primary,
|
||||||
click = { createProfileInProfiles(chatModel, displayName.value, close) },
|
click = {
|
||||||
|
if (chatModel.localUserCreated.value == true) {
|
||||||
|
createProfileInProfiles(chatModel, displayName.value, close)
|
||||||
|
} else {
|
||||||
|
createProfileInNoProfileSetup(displayName.value, close)
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
SectionTextFooter(generalGetString(MR.strings.your_profile_is_stored_on_your_device))
|
SectionTextFooter(generalGetString(MR.strings.your_profile_is_stored_on_your_device))
|
||||||
SectionTextFooter(generalGetString(MR.strings.profile_is_only_shared_with_your_contacts))
|
SectionTextFooter(generalGetString(MR.strings.profile_is_only_shared_with_your_contacts))
|
||||||
@@ -168,6 +174,17 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createProfileInNoProfileSetup(displayName: String, close: () -> Unit) {
|
||||||
|
withApi {
|
||||||
|
val user = controller.apiCreateActiveUser(null, Profile(displayName.trim(), "", null)) ?: return@withApi
|
||||||
|
controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
|
||||||
|
chatModel.chatRunning.value = false
|
||||||
|
controller.startChat(user)
|
||||||
|
controller.switchUIRemoteHost(null)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) {
|
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) {
|
||||||
withApi {
|
withApi {
|
||||||
val rhId = chatModel.remoteHostId()
|
val rhId = chatModel.remoteHostId()
|
||||||
|
|||||||
+1
-9
@@ -127,18 +127,10 @@ sealed class WCallResponse {
|
|||||||
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
|
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val protocolText: String get() {
|
|
||||||
val local = localCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
|
|
||||||
val localRelay = localCandidate?.relayProtocol?.uppercase(Locale.ROOT) ?: "unknown"
|
|
||||||
val remote = remoteCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
|
|
||||||
val localText = if (localRelay == local || localCandidate?.relayProtocol == null) local else "$local ($localRelay)"
|
|
||||||
return if (local == remote) localText else "$localText / $remote"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
||||||
@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?, val protocol: String?, val relayProtocol: String?)
|
@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?, val protocol: String?)
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
|
||||||
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
|
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
|
||||||
|
|
||||||
|
|||||||
+5
-24
@@ -68,14 +68,14 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
|||||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val (userPickerState, scaffoldState, switchingUsersAndHosts ) = settingsState
|
val (userPickerState, scaffoldState ) = settingsState
|
||||||
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } },
|
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||||
scaffoldState = scaffoldState,
|
scaffoldState = scaffoldState,
|
||||||
drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) },
|
drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) },
|
||||||
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
|
drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f),
|
||||||
drawerGesturesEnabled = appPlatform.isAndroid,
|
drawerGesturesEnabled = appPlatform.isAndroid,
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (searchInList.isEmpty()) {
|
if (searchInList.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (!stopped) {
|
if (!stopped) {
|
||||||
@@ -104,7 +104,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
|||||||
) {
|
) {
|
||||||
if (chatModel.chats.isNotEmpty()) {
|
if (chatModel.chats.isNotEmpty()) {
|
||||||
ChatList(chatModel, search = searchInList)
|
ChatList(chatModel, search = searchInList)
|
||||||
} else if (!switchingUsersAndHosts.value) {
|
} else if (!chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) {
|
if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) {
|
||||||
OnboardingButtons(showNewChatSheet)
|
OnboardingButtons(showNewChatSheet)
|
||||||
@@ -121,19 +121,11 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
|||||||
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
||||||
}
|
}
|
||||||
if (appPlatform.isAndroid) {
|
if (appPlatform.isAndroid) {
|
||||||
UserPicker(chatModel, userPickerState, switchingUsersAndHosts) {
|
UserPicker(chatModel, userPickerState) {
|
||||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||||
userPickerState.value = AnimatedViewState.GONE
|
userPickerState.value = AnimatedViewState.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (switchingUsersAndHosts.value) {
|
|
||||||
Box(
|
|
||||||
Modifier.fillMaxSize().clickable(enabled = false, onClick = {}),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
ProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -209,7 +201,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
|
|||||||
navigationButton = {
|
navigationButton = {
|
||||||
if (showSearch) {
|
if (showSearch) {
|
||||||
NavigationButtonBack(hideSearchOnBack)
|
NavigationButtonBack(hideSearchOnBack)
|
||||||
} else if (chatModel.users.isEmpty()) {
|
} else if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) {
|
||||||
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
|
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
|
||||||
} else {
|
} else {
|
||||||
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
||||||
@@ -304,17 +296,6 @@ private fun ToggleFilterButton() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ProgressIndicator() {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
Modifier
|
|
||||||
.padding(horizontal = 2.dp)
|
|
||||||
.size(30.dp),
|
|
||||||
color = MaterialTheme.colors.secondary,
|
|
||||||
strokeWidth = 2.5.dp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>)
|
expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
@Composable
|
@Composable
|
||||||
fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) {
|
fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) {
|
||||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||||
val (userPickerState, scaffoldState, switchingUsersAndHosts) = settingsState
|
val (userPickerState, scaffoldState) = settingsState
|
||||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||||
Scaffold(
|
Scaffold(
|
||||||
Modifier.padding(end = endPadding),
|
Modifier.padding(end = endPadding),
|
||||||
@@ -47,7 +47,7 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (appPlatform.isAndroid) {
|
if (appPlatform.isAndroid) {
|
||||||
UserPicker(chatModel, userPickerState, switchingUsersAndHosts, showSettings = false, showCancel = true, cancelClicked = {
|
UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = {
|
||||||
chatModel.sharedContent.value = null
|
chatModel.sharedContent.value = null
|
||||||
userPickerState.value = AnimatedViewState.GONE
|
userPickerState.value = AnimatedViewState.GONE
|
||||||
})
|
})
|
||||||
|
|||||||
+33
-20
@@ -26,7 +26,9 @@ import chat.simplex.common.model.ChatModel.controller
|
|||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.views.CreateProfile
|
||||||
import chat.simplex.common.views.remote.*
|
import chat.simplex.common.views.remote.*
|
||||||
|
import chat.simplex.common.views.usersettings.doWithAuth
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -38,7 +40,6 @@ import kotlin.math.roundToInt
|
|||||||
fun UserPicker(
|
fun UserPicker(
|
||||||
chatModel: ChatModel,
|
chatModel: ChatModel,
|
||||||
userPickerState: MutableStateFlow<AnimatedViewState>,
|
userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||||
switchingUsersAndHosts: MutableState<Boolean>,
|
|
||||||
showSettings: Boolean = true,
|
showSettings: Boolean = true,
|
||||||
showCancel: Boolean = false,
|
showCancel: Boolean = false,
|
||||||
cancelClicked: () -> Unit = {},
|
cancelClicked: () -> Unit = {},
|
||||||
@@ -123,14 +124,10 @@ fun UserPicker(
|
|||||||
userPickerState.value = AnimatedViewState.HIDING
|
userPickerState.value = AnimatedViewState.HIDING
|
||||||
if (!u.user.activeUser) {
|
if (!u.user.activeUser) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val job = launch {
|
controller.showProgressIfNeeded {
|
||||||
delay(500)
|
ModalManager.closeAllModalsEverywhere()
|
||||||
switchingUsersAndHosts.value = true
|
chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null)
|
||||||
}
|
}
|
||||||
ModalManager.closeAllModalsEverywhere()
|
|
||||||
chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null)
|
|
||||||
job.cancel()
|
|
||||||
switchingUsersAndHosts.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,13 +159,13 @@ fun UserPicker(
|
|||||||
val currentRemoteHost = remember { chatModel.currentRemoteHost }.value
|
val currentRemoteHost = remember { chatModel.currentRemoteHost }.value
|
||||||
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
|
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
|
||||||
if (remoteHosts.isNotEmpty()) {
|
if (remoteHosts.isNotEmpty()) {
|
||||||
if (currentRemoteHost == null) {
|
if (currentRemoteHost == null && chatModel.localUserCreated.value == true) {
|
||||||
LocalDevicePickerItem(true) {
|
LocalDevicePickerItem(true) {
|
||||||
userPickerState.value = AnimatedViewState.HIDING
|
userPickerState.value = AnimatedViewState.HIDING
|
||||||
switchToLocalDevice()
|
switchToLocalDevice()
|
||||||
}
|
}
|
||||||
Divider(Modifier.requiredHeight(1.dp))
|
Divider(Modifier.requiredHeight(1.dp))
|
||||||
} else {
|
} else if (currentRemoteHost != null) {
|
||||||
val connecting = rememberSaveable { mutableStateOf(false) }
|
val connecting = rememberSaveable { mutableStateOf(false) }
|
||||||
RemoteHostPickerItem(currentRemoteHost,
|
RemoteHostPickerItem(currentRemoteHost,
|
||||||
actionButtonClick = {
|
actionButtonClick = {
|
||||||
@@ -176,7 +173,7 @@ fun UserPicker(
|
|||||||
stopRemoteHostAndReloadHosts(currentRemoteHost, true)
|
stopRemoteHostAndReloadHosts(currentRemoteHost, true)
|
||||||
}) {
|
}) {
|
||||||
userPickerState.value = AnimatedViewState.HIDING
|
userPickerState.value = AnimatedViewState.HIDING
|
||||||
switchToRemoteHost(currentRemoteHost, switchingUsersAndHosts, connecting)
|
switchToRemoteHost(currentRemoteHost, connecting)
|
||||||
}
|
}
|
||||||
Divider(Modifier.requiredHeight(1.dp))
|
Divider(Modifier.requiredHeight(1.dp))
|
||||||
}
|
}
|
||||||
@@ -184,7 +181,7 @@ fun UserPicker(
|
|||||||
|
|
||||||
UsersView()
|
UsersView()
|
||||||
|
|
||||||
if (remoteHosts.isNotEmpty() && currentRemoteHost != null) {
|
if (remoteHosts.isNotEmpty() && currentRemoteHost != null && chatModel.localUserCreated.value == true) {
|
||||||
LocalDevicePickerItem(false) {
|
LocalDevicePickerItem(false) {
|
||||||
userPickerState.value = AnimatedViewState.HIDING
|
userPickerState.value = AnimatedViewState.HIDING
|
||||||
switchToLocalDevice()
|
switchToLocalDevice()
|
||||||
@@ -199,7 +196,7 @@ fun UserPicker(
|
|||||||
stopRemoteHostAndReloadHosts(h, false)
|
stopRemoteHostAndReloadHosts(h, false)
|
||||||
}) {
|
}) {
|
||||||
userPickerState.value = AnimatedViewState.HIDING
|
userPickerState.value = AnimatedViewState.HIDING
|
||||||
switchToRemoteHost(h, switchingUsersAndHosts, connecting)
|
switchToRemoteHost(h, connecting)
|
||||||
}
|
}
|
||||||
Divider(Modifier.requiredHeight(1.dp))
|
Divider(Modifier.requiredHeight(1.dp))
|
||||||
}
|
}
|
||||||
@@ -220,6 +217,18 @@ fun UserPicker(
|
|||||||
userPickerState.value = AnimatedViewState.GONE
|
userPickerState.value = AnimatedViewState.GONE
|
||||||
}
|
}
|
||||||
Divider(Modifier.requiredHeight(1.dp))
|
Divider(Modifier.requiredHeight(1.dp))
|
||||||
|
} else if (chatModel.desktopNoUserNoRemote) {
|
||||||
|
CreateInitialProfile {
|
||||||
|
doWithAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) {
|
||||||
|
ModalManager.center.showModalCloseable { close ->
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
userPickerState.value = AnimatedViewState.HIDING
|
||||||
|
}
|
||||||
|
CreateProfile(chat.simplex.common.platform.chatModel, close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider(Modifier.requiredHeight(1.dp))
|
||||||
}
|
}
|
||||||
if (showSettings) {
|
if (showSettings) {
|
||||||
SettingsPickerItem(settingsClicked)
|
SettingsPickerItem(settingsClicked)
|
||||||
@@ -401,6 +410,16 @@ private fun LinkAMobilePickerItem(onClick: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CreateInitialProfile(onClick: () -> Unit) {
|
||||||
|
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
||||||
|
val text = generalGetString(MR.strings.create_chat_profile)
|
||||||
|
Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||||
|
Spacer(Modifier.width(DEFAULT_PADDING + 6.dp))
|
||||||
|
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsPickerItem(onClick: () -> Unit) {
|
private fun SettingsPickerItem(onClick: () -> Unit) {
|
||||||
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
||||||
@@ -441,21 +460,15 @@ private fun switchToLocalDevice() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchToRemoteHost(h: RemoteHostInfo, switchingUsersAndHosts: MutableState<Boolean>, connecting: MutableState<Boolean>) {
|
private fun switchToRemoteHost(h: RemoteHostInfo, connecting: MutableState<Boolean>) {
|
||||||
if (!h.activeHost()) {
|
if (!h.activeHost()) {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val job = launch {
|
|
||||||
delay(500)
|
|
||||||
switchingUsersAndHosts.value = true
|
|
||||||
}
|
|
||||||
ModalManager.closeAllModalsEverywhere()
|
ModalManager.closeAllModalsEverywhere()
|
||||||
if (h.sessionState != null) {
|
if (h.sessionState != null) {
|
||||||
chatModel.controller.switchUIRemoteHost(h.remoteHostId)
|
chatModel.controller.switchUIRemoteHost(h.remoteHostId)
|
||||||
} else {
|
} else {
|
||||||
connectMobileDevice(h, connecting)
|
connectMobileDevice(h, connecting)
|
||||||
}
|
}
|
||||||
job.cancel()
|
|
||||||
switchingUsersAndHosts.value = false
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
connectMobileDevice(h, connecting)
|
connectMobileDevice(h, connecting)
|
||||||
|
|||||||
+2
-1
@@ -264,7 +264,8 @@ private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onCli
|
|||||||
text,
|
text,
|
||||||
generalGetString(MR.strings.enter_passphrase),
|
generalGetString(MR.strings.enter_passphrase),
|
||||||
isValid = ::validKey,
|
isValid = ::validKey,
|
||||||
keyboardActions = KeyboardActions(onDone = if (enabled) {
|
// Don't enable this on desktop since it interfere with key event listener
|
||||||
|
keyboardActions = KeyboardActions(onDone = if (enabled && appPlatform.isAndroid) {
|
||||||
{ onClick?.invoke() }
|
{ onClick?.invoke() }
|
||||||
} else null
|
} else null
|
||||||
),
|
),
|
||||||
|
|||||||
+138
-111
@@ -4,6 +4,7 @@ import SectionBottomSpacer
|
|||||||
import SectionDividerSpaced
|
import SectionDividerSpaced
|
||||||
import SectionTextFooter
|
import SectionTextFooter
|
||||||
import SectionItemView
|
import SectionItemView
|
||||||
|
import SectionSpacer
|
||||||
import SectionView
|
import SectionView
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -20,6 +21,7 @@ import androidx.compose.ui.text.*
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatModel.controller
|
||||||
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
@@ -59,7 +61,9 @@ fun DatabaseView(
|
|||||||
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) }
|
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) }
|
||||||
val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||||
if (to != null) {
|
if (to != null) {
|
||||||
importArchiveAlert(m, to, appFilesCountAndSize, progressIndicator)
|
importArchiveAlert(m, to, appFilesCountAndSize, progressIndicator) {
|
||||||
|
startChat(m, chatLastStart, m.chatDbChanged)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val chatItemTTL = remember { mutableStateOf(m.chatItemTTL.value) }
|
val chatItemTTL = remember { mutableStateOf(m.chatItemTTL.value) }
|
||||||
@@ -77,7 +81,6 @@ fun DatabaseView(
|
|||||||
m.chatDbEncrypted.value,
|
m.chatDbEncrypted.value,
|
||||||
m.controller.appPrefs.storeDBPassphrase.state.value,
|
m.controller.appPrefs.storeDBPassphrase.state.value,
|
||||||
m.controller.appPrefs.initialRandomDBPassphrase,
|
m.controller.appPrefs.initialRandomDBPassphrase,
|
||||||
m.controller.appPrefs.developerTools.state.value,
|
|
||||||
importArchiveLauncher,
|
importArchiveLauncher,
|
||||||
chatArchiveName,
|
chatArchiveName,
|
||||||
chatArchiveTime,
|
chatArchiveTime,
|
||||||
@@ -100,7 +103,13 @@ fun DatabaseView(
|
|||||||
setCiTTL(m, rhId, chatItemTTL, progressIndicator, appFilesCountAndSize)
|
setCiTTL(m, rhId, chatItemTTL, progressIndicator, appFilesCountAndSize)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showSettingsModal
|
showSettingsModal,
|
||||||
|
disconnectAllHosts = {
|
||||||
|
val connected = chatModel.remoteHosts.filter { it.sessionState is RemoteHostSessionState.Connected }
|
||||||
|
connected.forEachIndexed { index, h ->
|
||||||
|
controller.stopRemoteHostAndReloadHosts(h, index == connected.lastIndex && chatModel.connectedToRemote())
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
if (progressIndicator.value) {
|
if (progressIndicator.value) {
|
||||||
Box(
|
Box(
|
||||||
@@ -129,7 +138,6 @@ fun DatabaseLayout(
|
|||||||
chatDbEncrypted: Boolean?,
|
chatDbEncrypted: Boolean?,
|
||||||
passphraseSaved: Boolean,
|
passphraseSaved: Boolean,
|
||||||
initialRandomDBPassphrase: SharedPreference<Boolean>,
|
initialRandomDBPassphrase: SharedPreference<Boolean>,
|
||||||
developerTools: Boolean,
|
|
||||||
importArchiveLauncher: FileChooserLauncher,
|
importArchiveLauncher: FileChooserLauncher,
|
||||||
chatArchiveName: MutableState<String?>,
|
chatArchiveName: MutableState<String?>,
|
||||||
chatArchiveTime: MutableState<Instant?>,
|
chatArchiveTime: MutableState<Instant?>,
|
||||||
@@ -144,36 +152,43 @@ fun DatabaseLayout(
|
|||||||
deleteChatAlert: () -> Unit,
|
deleteChatAlert: () -> Unit,
|
||||||
deleteAppFilesAndMedia: () -> Unit,
|
deleteAppFilesAndMedia: () -> Unit,
|
||||||
onChatItemTTLSelected: (ChatItemTTL) -> Unit,
|
onChatItemTTLSelected: (ChatItemTTL) -> Unit,
|
||||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)
|
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||||
|
disconnectAllHosts: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val stopped = !runChat
|
val stopped = !runChat
|
||||||
val operationsDisabled = !stopped || progressIndicator
|
val operationsDisabled = (!stopped || progressIndicator) && !chatModel.desktopNoUserNoRemote
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
AppBarTitle(stringResource(MR.strings.your_chat_database))
|
AppBarTitle(stringResource(MR.strings.your_chat_database))
|
||||||
|
|
||||||
SectionView(stringResource(MR.strings.messages_section_title).uppercase()) {
|
if (!chatModel.desktopNoUserNoRemote) {
|
||||||
TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!stopped && !progressIndicator), onChatItemTTLSelected)
|
SectionView(stringResource(MR.strings.messages_section_title).uppercase()) {
|
||||||
}
|
TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!stopped && !progressIndicator), onChatItemTTLSelected)
|
||||||
SectionTextFooter(
|
|
||||||
remember(currentUser?.displayName) {
|
|
||||||
buildAnnotatedString {
|
|
||||||
append(generalGetString(MR.strings.messages_section_description) + " ")
|
|
||||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
|
||||||
append(currentUser?.displayName ?: "")
|
|
||||||
}
|
|
||||||
append(".")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
SectionTextFooter(
|
||||||
|
remember(currentUser?.displayName) {
|
||||||
if (currentRemoteHost == null) {
|
buildAnnotatedString {
|
||||||
|
append(generalGetString(MR.strings.messages_section_description) + " ")
|
||||||
|
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||||
|
append(currentUser?.displayName ?: "")
|
||||||
|
}
|
||||||
|
append(".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
SectionDividerSpaced(maxTopPadding = true)
|
SectionDividerSpaced(maxTopPadding = true)
|
||||||
|
}
|
||||||
|
val toggleEnabled = remember { chatModel.remoteHosts }.none { it.sessionState is RemoteHostSessionState.Connected }
|
||||||
|
if (chatModel.localUserCreated.value == true) {
|
||||||
SectionView(stringResource(MR.strings.run_chat_section)) {
|
SectionView(stringResource(MR.strings.run_chat_section)) {
|
||||||
RunChatSetting(runChat, stopped, startChat, stopChatAlert)
|
if (!toggleEnabled) {
|
||||||
|
SectionItemView(disconnectAllHosts) {
|
||||||
|
Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RunChatSetting(runChat, stopped, toggleEnabled, startChat, stopChatAlert)
|
||||||
}
|
}
|
||||||
SectionTextFooter(
|
SectionTextFooter(
|
||||||
if (stopped) {
|
if (stopped) {
|
||||||
@@ -183,92 +198,96 @@ fun DatabaseLayout(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
|
|
||||||
SectionView(stringResource(MR.strings.chat_database_section)) {
|
|
||||||
val unencrypted = chatDbEncrypted == false
|
|
||||||
SettingsActionItem(
|
|
||||||
if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled)
|
|
||||||
else painterResource(MR.images.ic_lock),
|
|
||||||
stringResource(MR.strings.database_passphrase),
|
|
||||||
click = showSettingsModal() { DatabaseEncryptionView(it) },
|
|
||||||
iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary,
|
|
||||||
disabled = operationsDisabled
|
|
||||||
)
|
|
||||||
if (appPlatform.isDesktop && developerTools) {
|
|
||||||
SettingsActionItem(
|
|
||||||
painterResource(MR.images.ic_folder_open),
|
|
||||||
stringResource(MR.strings.open_database_folder),
|
|
||||||
::desktopOpenDatabaseDir,
|
|
||||||
disabled = operationsDisabled
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SettingsActionItem(
|
|
||||||
painterResource(MR.images.ic_ios_share),
|
|
||||||
stringResource(MR.strings.export_database),
|
|
||||||
click = {
|
|
||||||
if (initialRandomDBPassphrase.get()) {
|
|
||||||
exportProhibitedAlert()
|
|
||||||
} else {
|
|
||||||
exportArchive()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
textColor = MaterialTheme.colors.primary,
|
|
||||||
iconColor = MaterialTheme.colors.primary,
|
|
||||||
disabled = operationsDisabled
|
|
||||||
)
|
|
||||||
SettingsActionItem(
|
|
||||||
painterResource(MR.images.ic_download),
|
|
||||||
stringResource(MR.strings.import_database),
|
|
||||||
{ withApi { importArchiveLauncher.launch("application/zip") } },
|
|
||||||
textColor = Color.Red,
|
|
||||||
iconColor = Color.Red,
|
|
||||||
disabled = operationsDisabled
|
|
||||||
)
|
|
||||||
val chatArchiveNameVal = chatArchiveName.value
|
|
||||||
val chatArchiveTimeVal = chatArchiveTime.value
|
|
||||||
val chatLastStartVal = chatLastStart.value
|
|
||||||
if (chatArchiveNameVal != null && chatArchiveTimeVal != null && chatLastStartVal != null) {
|
|
||||||
val title = chatArchiveTitle(chatArchiveTimeVal, chatLastStartVal)
|
|
||||||
SettingsActionItem(
|
|
||||||
painterResource(MR.images.ic_inventory_2),
|
|
||||||
title,
|
|
||||||
click = showSettingsModal { ChatArchiveView(it, title, chatArchiveNameVal, chatArchiveTimeVal) },
|
|
||||||
disabled = operationsDisabled
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SettingsActionItem(
|
|
||||||
painterResource(MR.images.ic_delete_forever),
|
|
||||||
stringResource(MR.strings.delete_database),
|
|
||||||
deleteChatAlert,
|
|
||||||
textColor = Color.Red,
|
|
||||||
iconColor = Color.Red,
|
|
||||||
disabled = operationsDisabled
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SectionDividerSpaced(maxTopPadding = true)
|
|
||||||
|
|
||||||
SectionView(stringResource(MR.strings.files_and_media_section).uppercase()) {
|
|
||||||
val deleteFilesDisabled = operationsDisabled || appFilesCountAndSize.value.first == 0
|
|
||||||
SectionItemView(
|
|
||||||
deleteAppFilesAndMedia,
|
|
||||||
disabled = deleteFilesDisabled
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(if (users.size > 1) MR.strings.delete_files_and_media_for_all_users else MR.strings.delete_files_and_media_all),
|
|
||||||
color = if (deleteFilesDisabled) MaterialTheme.colors.secondary else Color.Red
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val (count, size) = appFilesCountAndSize.value
|
|
||||||
SectionTextFooter(
|
|
||||||
if (count == 0) {
|
|
||||||
stringResource(MR.strings.no_received_app_files)
|
|
||||||
} else {
|
|
||||||
String.format(stringResource(MR.strings.total_files_count_and_size), count, formatBytes(size))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SectionView(stringResource(MR.strings.chat_database_section)) {
|
||||||
|
if (chatModel.localUserCreated.value != true && !toggleEnabled) {
|
||||||
|
SectionItemView(disconnectAllHosts) {
|
||||||
|
Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val unencrypted = chatDbEncrypted == false
|
||||||
|
SettingsActionItem(
|
||||||
|
if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled)
|
||||||
|
else painterResource(MR.images.ic_lock),
|
||||||
|
stringResource(MR.strings.database_passphrase),
|
||||||
|
click = showSettingsModal() { DatabaseEncryptionView(it) },
|
||||||
|
iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary,
|
||||||
|
disabled = operationsDisabled
|
||||||
|
)
|
||||||
|
if (appPlatform.isDesktop) {
|
||||||
|
SettingsActionItem(
|
||||||
|
painterResource(MR.images.ic_folder_open),
|
||||||
|
stringResource(MR.strings.open_database_folder),
|
||||||
|
::desktopOpenDatabaseDir,
|
||||||
|
disabled = operationsDisabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SettingsActionItem(
|
||||||
|
painterResource(MR.images.ic_ios_share),
|
||||||
|
stringResource(MR.strings.export_database),
|
||||||
|
click = {
|
||||||
|
if (initialRandomDBPassphrase.get()) {
|
||||||
|
exportProhibitedAlert()
|
||||||
|
} else {
|
||||||
|
exportArchive()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textColor = MaterialTheme.colors.primary,
|
||||||
|
iconColor = MaterialTheme.colors.primary,
|
||||||
|
disabled = operationsDisabled
|
||||||
|
)
|
||||||
|
SettingsActionItem(
|
||||||
|
painterResource(MR.images.ic_download),
|
||||||
|
stringResource(MR.strings.import_database),
|
||||||
|
{ withApi { importArchiveLauncher.launch("application/zip") } },
|
||||||
|
textColor = Color.Red,
|
||||||
|
iconColor = Color.Red,
|
||||||
|
disabled = operationsDisabled
|
||||||
|
)
|
||||||
|
val chatArchiveNameVal = chatArchiveName.value
|
||||||
|
val chatArchiveTimeVal = chatArchiveTime.value
|
||||||
|
val chatLastStartVal = chatLastStart.value
|
||||||
|
if (chatArchiveNameVal != null && chatArchiveTimeVal != null && chatLastStartVal != null) {
|
||||||
|
val title = chatArchiveTitle(chatArchiveTimeVal, chatLastStartVal)
|
||||||
|
SettingsActionItem(
|
||||||
|
painterResource(MR.images.ic_inventory_2),
|
||||||
|
title,
|
||||||
|
click = showSettingsModal { ChatArchiveView(it, title, chatArchiveNameVal, chatArchiveTimeVal) },
|
||||||
|
disabled = operationsDisabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SettingsActionItem(
|
||||||
|
painterResource(MR.images.ic_delete_forever),
|
||||||
|
stringResource(MR.strings.delete_database),
|
||||||
|
deleteChatAlert,
|
||||||
|
textColor = Color.Red,
|
||||||
|
iconColor = Color.Red,
|
||||||
|
disabled = operationsDisabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SectionDividerSpaced(maxTopPadding = true)
|
||||||
|
|
||||||
|
SectionView(stringResource(MR.strings.files_and_media_section).uppercase()) {
|
||||||
|
val deleteFilesDisabled = operationsDisabled || appFilesCountAndSize.value.first == 0
|
||||||
|
SectionItemView(
|
||||||
|
deleteAppFilesAndMedia,
|
||||||
|
disabled = deleteFilesDisabled
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(if (users.size > 1) MR.strings.delete_files_and_media_for_all_users else MR.strings.delete_files_and_media_all),
|
||||||
|
color = if (deleteFilesDisabled) MaterialTheme.colors.secondary else Color.Red
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val (count, size) = appFilesCountAndSize.value
|
||||||
|
SectionTextFooter(
|
||||||
|
if (count == 0) {
|
||||||
|
stringResource(MR.strings.no_received_app_files)
|
||||||
|
} else {
|
||||||
|
String.format(stringResource(MR.strings.total_files_count_and_size), count, formatBytes(size))
|
||||||
|
}
|
||||||
|
)
|
||||||
SectionBottomSpacer()
|
SectionBottomSpacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,6 +338,7 @@ private fun TtlOptions(current: State<ChatItemTTL>, enabled: State<Boolean>, onS
|
|||||||
fun RunChatSetting(
|
fun RunChatSetting(
|
||||||
runChat: Boolean,
|
runChat: Boolean,
|
||||||
stopped: Boolean,
|
stopped: Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
startChat: () -> Unit,
|
startChat: () -> Unit,
|
||||||
stopChatAlert: () -> Unit
|
stopChatAlert: () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -337,6 +357,7 @@ fun RunChatSetting(
|
|||||||
stopChatAlert()
|
stopChatAlert()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
enabled = enabled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,13 +522,14 @@ private fun importArchiveAlert(
|
|||||||
m: ChatModel,
|
m: ChatModel,
|
||||||
importedArchiveURI: URI,
|
importedArchiveURI: URI,
|
||||||
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
||||||
progressIndicator: MutableState<Boolean>
|
progressIndicator: MutableState<Boolean>,
|
||||||
|
startChat: () -> Unit,
|
||||||
) {
|
) {
|
||||||
AlertManager.shared.showAlertDialog(
|
AlertManager.shared.showAlertDialog(
|
||||||
title = generalGetString(MR.strings.import_database_question),
|
title = generalGetString(MR.strings.import_database_question),
|
||||||
text = generalGetString(MR.strings.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one),
|
text = generalGetString(MR.strings.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one),
|
||||||
confirmText = generalGetString(MR.strings.import_database_confirmation),
|
confirmText = generalGetString(MR.strings.import_database_confirmation),
|
||||||
onConfirm = { importArchive(m, importedArchiveURI, appFilesCountAndSize, progressIndicator) },
|
onConfirm = { importArchive(m, importedArchiveURI, appFilesCountAndSize, progressIndicator, startChat) },
|
||||||
destructive = true,
|
destructive = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -516,7 +538,8 @@ private fun importArchive(
|
|||||||
m: ChatModel,
|
m: ChatModel,
|
||||||
importedArchiveURI: URI,
|
importedArchiveURI: URI,
|
||||||
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
||||||
progressIndicator: MutableState<Boolean>
|
progressIndicator: MutableState<Boolean>,
|
||||||
|
startChat: () -> Unit,
|
||||||
) {
|
) {
|
||||||
progressIndicator.value = true
|
progressIndicator.value = true
|
||||||
val archivePath = saveArchiveFromURI(importedArchiveURI)
|
val archivePath = saveArchiveFromURI(importedArchiveURI)
|
||||||
@@ -533,6 +556,10 @@ private fun importArchive(
|
|||||||
operationEnded(m, progressIndicator) {
|
operationEnded(m, progressIndicator) {
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database))
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database))
|
||||||
}
|
}
|
||||||
|
if (chatModel.localUserCreated.value == false) {
|
||||||
|
chatModel.chatRunning.value = false
|
||||||
|
startChat()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
operationEnded(m, progressIndicator) {
|
operationEnded(m, progressIndicator) {
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database) + "\n" + generalGetString(MR.strings.non_fatal_errors_occured_during_import))
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database) + "\n" + generalGetString(MR.strings.non_fatal_errors_occured_during_import))
|
||||||
@@ -681,7 +708,6 @@ fun PreviewDatabaseLayout() {
|
|||||||
chatDbEncrypted = false,
|
chatDbEncrypted = false,
|
||||||
passphraseSaved = false,
|
passphraseSaved = false,
|
||||||
initialRandomDBPassphrase = SharedPreference({ true }, {}),
|
initialRandomDBPassphrase = SharedPreference({ true }, {}),
|
||||||
developerTools = true,
|
|
||||||
importArchiveLauncher = rememberFileChooserLauncher(true) {},
|
importArchiveLauncher = rememberFileChooserLauncher(true) {},
|
||||||
chatArchiveName = remember { mutableStateOf("dummy_archive") },
|
chatArchiveName = remember { mutableStateOf("dummy_archive") },
|
||||||
chatArchiveTime = remember { mutableStateOf(Clock.System.now()) },
|
chatArchiveTime = remember { mutableStateOf(Clock.System.now()) },
|
||||||
@@ -697,6 +723,7 @@ fun PreviewDatabaseLayout() {
|
|||||||
deleteAppFilesAndMedia = {},
|
deleteAppFilesAndMedia = {},
|
||||||
showSettingsModal = { {} },
|
showSettingsModal = { {} },
|
||||||
onChatItemTTLSelected = {},
|
onChatItemTTLSelected = {},
|
||||||
|
disconnectAllHosts = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -152,7 +152,6 @@ fun DefaultConfigurableTextField(
|
|||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = state.value,
|
value = state.value,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
|
||||||
.background(colors.backgroundColor(enabled).value, shape)
|
.background(colors.backgroundColor(enabled).value, shape)
|
||||||
.indicatorLine(enabled, false, interactionSource, colors)
|
.indicatorLine(enabled, false, interactionSource, colors)
|
||||||
.defaultMinSize(
|
.defaultMinSize(
|
||||||
|
|||||||
+66
-51
@@ -10,72 +10,87 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.*
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> ExposedDropDownSetting(
|
||||||
|
values: List<Pair<T, String>>,
|
||||||
|
selection: State<T>,
|
||||||
|
textColor: Color = MaterialTheme.colors.secondary,
|
||||||
|
label: String? = null,
|
||||||
|
enabled: State<Boolean> = mutableStateOf(true),
|
||||||
|
minWidth: Dp = 200.dp,
|
||||||
|
maxWidth: Dp = with(LocalDensity.current) { 180.sp.toDp() },
|
||||||
|
onSelected: (T) -> Unit
|
||||||
|
) {
|
||||||
|
val expanded = remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded.value,
|
||||||
|
onExpandedChange = {
|
||||||
|
expanded.value = !expanded.value && enabled.value
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier.padding(start = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""),
|
||||||
|
Modifier.widthIn(max = maxWidth),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = textColor
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(12.dp))
|
||||||
|
Icon(
|
||||||
|
if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less),
|
||||||
|
generalGetString(MR.strings.icon_descr_more_button),
|
||||||
|
tint = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DefaultExposedDropdownMenu(
|
||||||
|
modifier = Modifier.widthIn(min = minWidth),
|
||||||
|
expanded = expanded,
|
||||||
|
) {
|
||||||
|
values.forEach { selectionOption ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
onSelected(selectionOption.first)
|
||||||
|
expanded.value = false
|
||||||
|
},
|
||||||
|
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
selectionOption.second + (if (label != null) " $label" else ""),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> ExposedDropDownSettingRow(
|
fun <T> ExposedDropDownSettingRow(
|
||||||
title: String,
|
title: String,
|
||||||
values: List<Pair<T, String>>,
|
values: List<Pair<T, String>>,
|
||||||
selection: State<T>,
|
selection: State<T>,
|
||||||
|
textColor: Color = MaterialTheme.colors.secondary,
|
||||||
label: String? = null,
|
label: String? = null,
|
||||||
icon: Painter? = null,
|
icon: Painter? = null,
|
||||||
iconTint: Color = MaterialTheme.colors.secondary,
|
iconTint: Color = MaterialTheme.colors.secondary,
|
||||||
enabled: State<Boolean> = mutableStateOf(true),
|
enabled: State<Boolean> = mutableStateOf(true),
|
||||||
|
minWidth: Dp = 200.dp,
|
||||||
|
maxWidth: Dp = with(LocalDensity.current) { 180.sp.toDp() },
|
||||||
onSelected: (T) -> Unit
|
onSelected: (T) -> Unit
|
||||||
) {
|
) {
|
||||||
SettingsActionItemWithContent(icon, title, iconColor = iconTint, disabled = !enabled.value) {
|
SettingsActionItemWithContent(icon, title, iconColor = iconTint, disabled = !enabled.value) {
|
||||||
val expanded = remember { mutableStateOf(false) }
|
ExposedDropDownSetting(values, selection ,textColor, label, enabled, minWidth, maxWidth, onSelected)
|
||||||
ExposedDropdownMenuBox(
|
|
||||||
expanded = expanded.value,
|
|
||||||
onExpandedChange = {
|
|
||||||
expanded.value = !expanded.value && enabled.value
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
Modifier.padding(start = 10.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.End
|
|
||||||
) {
|
|
||||||
val maxWidth = with(LocalDensity.current) { 180.sp.toDp() }
|
|
||||||
Text(
|
|
||||||
values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""),
|
|
||||||
Modifier.widthIn(max = maxWidth),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
color = MaterialTheme.colors.secondary
|
|
||||||
)
|
|
||||||
Spacer(Modifier.size(12.dp))
|
|
||||||
Icon(
|
|
||||||
if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less),
|
|
||||||
generalGetString(MR.strings.icon_descr_more_button),
|
|
||||||
tint = MaterialTheme.colors.secondary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DefaultExposedDropdownMenu(
|
|
||||||
modifier = Modifier.widthIn(min = 200.dp),
|
|
||||||
expanded = expanded,
|
|
||||||
) {
|
|
||||||
values.forEach { selectionOption ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
onClick = {
|
|
||||||
onSelected(selectionOption.first)
|
|
||||||
expanded.value = false
|
|
||||||
},
|
|
||||||
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
selectionOption.second + (if (label != null) " $label" else ""),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.painter.Painter
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import chat.simplex.common.platform.onRightClick
|
import chat.simplex.common.platform.onRightClick
|
||||||
@@ -202,13 +203,14 @@ fun SectionTextFooter(text: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionTextFooter(text: AnnotatedString) {
|
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) {
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
|
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
|
||||||
color = MaterialTheme.colors.secondary,
|
color = MaterialTheme.colors.secondary,
|
||||||
lineHeight = 18.sp,
|
lineHeight = 18.sp,
|
||||||
fontSize = 14.sp
|
fontSize = 14.sp,
|
||||||
|
textAlign = textAlign
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -55,7 +55,7 @@ fun annotatedStringResource(id: StringResource): AnnotatedString {
|
|||||||
@Composable
|
@Composable
|
||||||
fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString {
|
fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
return remember(id) {
|
return remember(id, args) {
|
||||||
escapedHtmlToAnnotatedString(id.localized().format(args = args), density)
|
escapedHtmlToAnnotatedString(id.localized().format(args = args), density)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -108,6 +108,7 @@ private fun createInvitation(
|
|||||||
withApi {
|
withApi {
|
||||||
val r = m.controller.apiAddContact(rhId, incognito = m.controller.appPrefs.incognito.get())
|
val r = m.controller.apiAddContact(rhId, incognito = m.controller.appPrefs.incognito.get())
|
||||||
if (r != null) {
|
if (r != null) {
|
||||||
|
m.updateContactConnection(rhId, r.second)
|
||||||
connReqInvitation.value = r.first
|
connReqInvitation.value = r.first
|
||||||
contactConnection.value = r.second
|
contactConnection.value = r.second
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+4
-4
@@ -283,10 +283,11 @@ suspend fun connectViaUri(
|
|||||||
incognito: Boolean,
|
incognito: Boolean,
|
||||||
connectionPlan: ConnectionPlan?,
|
connectionPlan: ConnectionPlan?,
|
||||||
close: (() -> Unit)?
|
close: (() -> Unit)?
|
||||||
): Boolean {
|
) {
|
||||||
val r = chatModel.controller.apiConnect(rhId, incognito, uri.toString())
|
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString())
|
||||||
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
|
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
|
||||||
if (r) {
|
if (pcc != null) {
|
||||||
|
chatModel.updateContactConnection(rhId, pcc)
|
||||||
close?.invoke()
|
close?.invoke()
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
title = generalGetString(MR.strings.connection_request_sent),
|
title = generalGetString(MR.strings.connection_request_sent),
|
||||||
@@ -299,7 +300,6 @@ suspend fun connectViaUri(
|
|||||||
hostDevice = hostDevice(rhId),
|
hostDevice = hostDevice(rhId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType {
|
fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType {
|
||||||
|
|||||||
+4
@@ -182,6 +182,10 @@ private fun prepareChatBeforeAddressCreation(rhId: Long?) {
|
|||||||
val user = chatModel.controller.apiGetActiveUser(rhId) ?: return@withApi
|
val user = chatModel.controller.apiGetActiveUser(rhId) ?: return@withApi
|
||||||
chatModel.currentUser.value = user
|
chatModel.currentUser.value = user
|
||||||
if (chatModel.users.isEmpty()) {
|
if (chatModel.users.isEmpty()) {
|
||||||
|
if (appPlatform.isDesktop) {
|
||||||
|
// Make possible to use chat after going to remote device linking and returning back to local profile creation
|
||||||
|
chatModel.chatRunning.value = false
|
||||||
|
}
|
||||||
chatModel.controller.startChat(user)
|
chatModel.controller.startChat(user)
|
||||||
} else {
|
} else {
|
||||||
val users = chatModel.controller.listUsers(rhId)
|
val users = chatModel.controller.listUsers(rhId)
|
||||||
|
|||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
package chat.simplex.common.views.onboarding
|
||||||
|
|
||||||
|
import SectionTextFooter
|
||||||
|
import SectionView
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.simplex.common.model.ChatModel
|
||||||
|
import chat.simplex.common.platform.chatModel
|
||||||
|
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||||
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.common.views.remote.AddingMobileDevice
|
||||||
|
import chat.simplex.common.views.remote.DeviceNameField
|
||||||
|
import chat.simplex.common.views.usersettings.PreferenceToggle
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LinkAMobile() {
|
||||||
|
val connecting = rememberSaveable { mutableStateOf(false) }
|
||||||
|
val deviceName = chatModel.controller.appPrefs.deviceNameForRemoteAccess
|
||||||
|
var deviceNameInQrCode by remember { mutableStateOf(chatModel.controller.appPrefs.deviceNameForRemoteAccess.get()) }
|
||||||
|
val staleQrCode = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LinkAMobileLayout(
|
||||||
|
deviceName = remember { deviceName.state },
|
||||||
|
connecting,
|
||||||
|
staleQrCode,
|
||||||
|
updateDeviceName = {
|
||||||
|
withBGApi {
|
||||||
|
if (it != "" && it != deviceName.get()) {
|
||||||
|
chatModel.controller.setLocalDeviceName(it)
|
||||||
|
deviceName.set(it)
|
||||||
|
staleQrCode.value = deviceName.get() != deviceNameInQrCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
KeyChangeEffect(staleQrCode.value) {
|
||||||
|
if (!staleQrCode.value) {
|
||||||
|
deviceNameInQrCode = deviceName.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LinkAMobileLayout(
|
||||||
|
deviceName: State<String?>,
|
||||||
|
connecting: MutableState<Boolean>,
|
||||||
|
staleQrCode: MutableState<Boolean>,
|
||||||
|
updateDeviceName: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(top = 20.dp)) {
|
||||||
|
AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles))
|
||||||
|
Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Column(
|
||||||
|
Modifier.weight(0.3f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
SectionView(generalGetString(MR.strings.this_device_name).uppercase()) {
|
||||||
|
DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) }
|
||||||
|
SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile))
|
||||||
|
PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) {
|
||||||
|
ChatModel.controller.appPrefs.offerRemoteMulticast.set(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(Modifier.weight(0.7f)) {
|
||||||
|
AddingMobileDevice(false, staleQrCode, connecting) {
|
||||||
|
if (chatModel.remoteHosts.isEmpty()) {
|
||||||
|
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||||
|
} else {
|
||||||
|
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SimpleButtonDecorated(
|
||||||
|
text = stringResource(MR.strings.about_simplex),
|
||||||
|
icon = painterResource(MR.images.ic_arrow_back_ios_new),
|
||||||
|
textDecoration = TextDecoration.None,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) }
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -3,6 +3,7 @@ package chat.simplex.common.views.onboarding
|
|||||||
enum class OnboardingStage {
|
enum class OnboardingStage {
|
||||||
Step1_SimpleXInfo,
|
Step1_SimpleXInfo,
|
||||||
Step2_CreateProfile,
|
Step2_CreateProfile,
|
||||||
|
LinkAMobile,
|
||||||
Step2_5_SetupDatabasePassphrase,
|
Step2_5_SetupDatabasePassphrase,
|
||||||
Step3_CreateSimpleXAddress,
|
Step3_CreateSimpleXAddress,
|
||||||
Step4_SetNotificationsMode,
|
Step4_SetNotificationsMode,
|
||||||
|
|||||||
+6
-5
@@ -43,7 +43,11 @@ fun SetupDatabasePassphrase(m: ChatModel) {
|
|||||||
val newKey = rememberSaveable { mutableStateOf("") }
|
val newKey = rememberSaveable { mutableStateOf("") }
|
||||||
val confirmNewKey = rememberSaveable { mutableStateOf("") }
|
val confirmNewKey = rememberSaveable { mutableStateOf("") }
|
||||||
fun nextStep() {
|
fun nextStep() {
|
||||||
m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
|
if (appPlatform.isAndroid || chatModel.currentUser.value != null) {
|
||||||
|
m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
|
||||||
|
} else {
|
||||||
|
m.controller.appPrefs.onboardingStage.set(OnboardingStage.LinkAMobile)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SetupDatabasePassphraseLayout(
|
SetupDatabasePassphraseLayout(
|
||||||
currentKey,
|
currentKey,
|
||||||
@@ -159,10 +163,7 @@ private fun SetupDatabasePassphraseLayout(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value },
|
isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value },
|
||||||
keyboardActions = KeyboardActions(onDone = {
|
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }),
|
||||||
if (!disabled) onClickUpdate()
|
|
||||||
defaultKeyboardAction(ImeAction.Done)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) {
|
Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) {
|
||||||
|
|||||||
+8
-8
@@ -8,6 +8,7 @@ import androidx.compose.material.*
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
@@ -99,26 +100,22 @@ private fun InfoRow(icon: Painter, titleId: StringResource, textId: StringResour
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)? = null) {
|
expect fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)? = null)
|
||||||
if (user == null) {
|
|
||||||
OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, onclick)
|
|
||||||
} else {
|
|
||||||
OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OnboardingActionButton(
|
fun OnboardingActionButton(
|
||||||
labelId: StringResource,
|
labelId: StringResource,
|
||||||
onboarding: OnboardingStage?,
|
onboarding: OnboardingStage?,
|
||||||
border: Boolean,
|
border: Boolean,
|
||||||
|
icon: Painter? = null,
|
||||||
|
iconColor: Color = MaterialTheme.colors.primary,
|
||||||
onclick: (() -> Unit)?
|
onclick: (() -> Unit)?
|
||||||
) {
|
) {
|
||||||
val modifier = if (border) {
|
val modifier = if (border) {
|
||||||
Modifier
|
Modifier
|
||||||
.border(border = BorderStroke(1.dp, MaterialTheme.colors.primary), shape = RoundedCornerShape(50))
|
.border(border = BorderStroke(1.dp, MaterialTheme.colors.primary), shape = RoundedCornerShape(50))
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = DEFAULT_PADDING * 2,
|
horizontal = if (icon == null) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF,
|
||||||
vertical = 4.dp
|
vertical = 4.dp
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -131,6 +128,9 @@ fun OnboardingActionButton(
|
|||||||
ChatController.appPrefs.onboardingStage.set(onboarding)
|
ChatController.appPrefs.onboardingStage.set(onboarding)
|
||||||
}
|
}
|
||||||
}, modifier) {
|
}, modifier) {
|
||||||
|
if (icon != null) {
|
||||||
|
Icon(icon, stringResource(labelId), Modifier.padding(end = DEFAULT_PADDING_HALF), tint = iconColor)
|
||||||
|
}
|
||||||
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary, fontSize = 20.sp)
|
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary, fontSize = 20.sp)
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(MR.images.ic_arrow_forward_ios), "next stage", tint = MaterialTheme.colors.primary,
|
painterResource(MR.images.ic_arrow_forward_ios), "next stage", tint = MaterialTheme.colors.primary,
|
||||||
|
|||||||
+205
-77
@@ -9,16 +9,20 @@ import SectionView
|
|||||||
import TextIconSpaced
|
import TextIconSpaced
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.*
|
import androidx.compose.ui.text.font.*
|
||||||
import androidx.compose.ui.text.input.*
|
import androidx.compose.ui.text.input.*
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
@@ -30,11 +34,11 @@ import chat.simplex.common.views.chat.item.ItemAction
|
|||||||
import chat.simplex.common.views.chatlist.*
|
import chat.simplex.common.views.chatlist.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.newchat.QRCode
|
import chat.simplex.common.views.newchat.QRCode
|
||||||
import chat.simplex.common.views.usersettings.PreferenceToggle
|
import chat.simplex.common.views.usersettings.*
|
||||||
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectMobileView() {
|
fun ConnectMobileView() {
|
||||||
@@ -97,9 +101,11 @@ fun ConnectMobileLayout(
|
|||||||
SectionDividerSpaced(maxBottomPadding = false)
|
SectionDividerSpaced(maxBottomPadding = false)
|
||||||
}
|
}
|
||||||
SectionView(stringResource(MR.strings.devices).uppercase()) {
|
SectionView(stringResource(MR.strings.devices).uppercase()) {
|
||||||
SettingsActionItemWithContent(text = stringResource(MR.strings.this_device), icon = painterResource(MR.images.ic_desktop), click = connectDesktop) {
|
if (chatModel.localUserCreated.value == true) {
|
||||||
if (connectedHost.value == null) {
|
SettingsActionItemWithContent(text = stringResource(MR.strings.this_device), icon = painterResource(MR.images.ic_desktop), click = connectDesktop) {
|
||||||
Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
if (connectedHost.value == null) {
|
||||||
|
Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +158,7 @@ fun DeviceNameField(
|
|||||||
DefaultConfigurableTextField(
|
DefaultConfigurableTextField(
|
||||||
state = state,
|
state = state,
|
||||||
placeholder = generalGetString(MR.strings.enter_this_device_name),
|
placeholder = generalGetString(MR.strings.enter_this_device_name),
|
||||||
modifier = Modifier.padding(start = DEFAULT_PADDING),
|
modifier = Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING),
|
||||||
isValid = { true },
|
isValid = { true },
|
||||||
)
|
)
|
||||||
KeyChangeEffect(state.value) {
|
KeyChangeEffect(state.value) {
|
||||||
@@ -162,26 +168,51 @@ fun DeviceNameField(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ConnectMobileViewLayout(
|
private fun ConnectMobileViewLayout(
|
||||||
title: String,
|
title: String?,
|
||||||
invitation: String?,
|
invitation: String?,
|
||||||
deviceName: String?,
|
deviceName: String?,
|
||||||
sessionCode: String?,
|
sessionCode: String?,
|
||||||
port: String?
|
port: String?,
|
||||||
|
staleQrCode: Boolean = false,
|
||||||
|
editEnabled: Boolean = false,
|
||||||
|
editClicked: () -> Unit = {},
|
||||||
|
refreshQrCode: () -> Unit = {},
|
||||||
|
UnderQrLayout: @Composable () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
AppBarTitle(title)
|
if (title != null) {
|
||||||
|
AppBarTitle(title)
|
||||||
|
}
|
||||||
SectionView {
|
SectionView {
|
||||||
if (invitation != null && sessionCode == null && port != null) {
|
if (invitation != null && sessionCode == null && port != null) {
|
||||||
QRCode(
|
Box {
|
||||||
invitation, Modifier
|
QRCode(
|
||||||
.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF)
|
invitation, Modifier
|
||||||
.aspectRatio(1f)
|
.padding(start = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF)
|
||||||
)
|
.aspectRatio(1f)
|
||||||
SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code))
|
)
|
||||||
SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port))
|
if (staleQrCode) {
|
||||||
|
Box(Modifier.matchParentSize().background(MaterialTheme.colors.background.copy(alpha = 0.9f)), contentAlignment = Alignment.Center) {
|
||||||
|
SimpleButtonDecorated(stringResource(MR.strings.refresh_qr_code), painterResource(MR.images.ic_refresh), click = refreshQrCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SectionTextFooter(annotatedStringResource(MR.strings.open_on_mobile_and_scan_qr_code), textAlign = TextAlign.Center)
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
SectionTextFooter(annotatedStringResource(MR.strings.waiting_for_mobile_to_connect_on_port, port), textAlign = TextAlign.Center)
|
||||||
|
if (editEnabled) {
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
IconButton(editClicked, Modifier.size(16.dp)) {
|
||||||
|
Icon(painterResource(MR.images.ic_edit), stringResource(MR.strings.edit_verb), Modifier.size(16.dp), tint = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UnderQrLayout()
|
||||||
|
|
||||||
if (remember { controller.appPrefs.developerTools.state }.value) {
|
if (remember { controller.appPrefs.developerTools.state }.value) {
|
||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
@@ -218,6 +249,7 @@ private fun ConnectMobileViewLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SectionBottomSpacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,64 +269,110 @@ fun connectMobileDevice(rh: RemoteHostInfo, connecting: MutableState<Boolean>) {
|
|||||||
|
|
||||||
private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
|
private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
|
||||||
ModalManager.start.showModalCloseable { close ->
|
ModalManager.start.showModalCloseable { close ->
|
||||||
val invitation = rememberSaveable { mutableStateOf<String?>(null) }
|
AddingMobileDevice(true, remember { mutableStateOf(false) }, connecting, close)
|
||||||
val port = rememberSaveable { mutableStateOf<String?>(null) }
|
}
|
||||||
val pairing = remember { chatModel.remoteHostPairing }
|
}
|
||||||
val sessionCode = when (val state = pairing.value?.second) {
|
|
||||||
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
|
@Composable
|
||||||
else -> null
|
fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, connecting: MutableState<Boolean>, close: () -> Unit) {
|
||||||
}
|
val cachedR = remember { mutableStateOf<CR.RemoteHostStarted?>(null) }
|
||||||
/** It's needed to prevent screen flashes when [chatModel.newRemoteHostPairing] sets to null in background */
|
val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) }
|
||||||
var cachedSessionCode by remember { mutableStateOf<String?>(null) }
|
val customPort = rememberSaveable { mutableStateOf<Int?>(null) }
|
||||||
if (cachedSessionCode == null && sessionCode != null) {
|
var editing by rememberSaveable { mutableStateOf(false) }
|
||||||
cachedSessionCode = sessionCode
|
val startRemoteHost = suspend {
|
||||||
}
|
val r = chatModel.controller.startRemoteHost(
|
||||||
val remoteDeviceName = pairing.value?.first?.hostDeviceName
|
rhId = null,
|
||||||
ConnectMobileViewLayout(
|
multicast = controller.appPrefs.offerRemoteMulticast.get(),
|
||||||
title = if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection),
|
address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_,
|
||||||
invitation = invitation.value,
|
port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_
|
||||||
deviceName = remoteDeviceName,
|
|
||||||
sessionCode = cachedSessionCode,
|
|
||||||
port = port.value
|
|
||||||
)
|
)
|
||||||
val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) }
|
if (r != null) {
|
||||||
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
|
cachedR.value = r
|
||||||
if (chatModel.currentRemoteHost.value?.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != oldRemoteHostId) {
|
connecting.value = true
|
||||||
close()
|
customAddress.value = cachedR.address
|
||||||
}
|
customPort.value = cachedR.port
|
||||||
|
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
|
||||||
}
|
}
|
||||||
KeyChangeEffect(pairing.value) {
|
}
|
||||||
if (pairing.value == null) {
|
val pairing = remember { chatModel.remoteHostPairing }
|
||||||
close()
|
val sessionCode = when (val state = pairing.value?.second) {
|
||||||
}
|
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
|
||||||
}
|
else -> null
|
||||||
DisposableEffect(Unit) {
|
}
|
||||||
|
/** It's needed to prevent screen flashes when [chatModel.newRemoteHostPairing] sets to null in background */
|
||||||
|
var cachedSessionCode by remember { mutableStateOf<String?>(null) }
|
||||||
|
if (cachedSessionCode == null && sessionCode != null) {
|
||||||
|
cachedSessionCode = sessionCode
|
||||||
|
}
|
||||||
|
val remoteDeviceName = pairing.value?.first?.hostDeviceName
|
||||||
|
ConnectMobileViewLayout(
|
||||||
|
title = if (!showTitle) null else if (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection),
|
||||||
|
invitation = cachedR.invitation,
|
||||||
|
deviceName = remoteDeviceName,
|
||||||
|
sessionCode = cachedSessionCode,
|
||||||
|
port = cachedR.value?.ctrlPort,
|
||||||
|
staleQrCode = staleQrCode.value || (cachedR.address != customAddress.value && customAddress.value != null) || (cachedR.port != customPort.value && customPort.value != null),
|
||||||
|
editEnabled = !editing && cachedR.addresses.isNotEmpty(),
|
||||||
|
editClicked = { editing = true },
|
||||||
|
refreshQrCode = {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val r = chatModel.controller.startRemoteHost(null, controller.appPrefs.offerRemoteMulticast.get())
|
if (chatController.stopRemoteHost(null)) {
|
||||||
if (r != null) {
|
startRemoteHost()
|
||||||
connecting.value = true
|
staleQrCode.value = false
|
||||||
invitation.value = r.second
|
editing = false
|
||||||
port.value = r.third
|
|
||||||
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onDispose {
|
},
|
||||||
if (chatModel.currentRemoteHost.value?.remoteHostId == oldRemoteHostId) {
|
UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) }
|
||||||
withBGApi {
|
)
|
||||||
chatController.stopRemoteHost(null)
|
val oldRemoteHostId by remember { mutableStateOf(chatModel.currentRemoteHost.value?.remoteHostId) }
|
||||||
}
|
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
|
||||||
|
if (chatModel.currentRemoteHost.value?.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != oldRemoteHostId) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyChangeEffect(pairing.value) {
|
||||||
|
if (pairing.value == null) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
withBGApi {
|
||||||
|
startRemoteHost()
|
||||||
|
}
|
||||||
|
onDispose {
|
||||||
|
if (chatModel.currentRemoteHost.value?.remoteHostId == oldRemoteHostId) {
|
||||||
|
withBGApi {
|
||||||
|
chatController.stopRemoteHost(null)
|
||||||
}
|
}
|
||||||
chatModel.remoteHostPairing.value = null
|
|
||||||
}
|
}
|
||||||
|
chatModel.remoteHostPairing.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState<Boolean>) {
|
private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState<Boolean>) {
|
||||||
ModalManager.start.showModalCloseable { close ->
|
ModalManager.start.showModalCloseable { close ->
|
||||||
|
val cachedR = remember { mutableStateOf<CR.RemoteHostStarted?>(null) }
|
||||||
|
val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) }
|
||||||
|
val customPort = rememberSaveable { mutableStateOf<Int?>(null) }
|
||||||
|
var editing by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val startRemoteHost = suspend {
|
||||||
|
val r = chatModel.controller.startRemoteHost(
|
||||||
|
rhId = rh.remoteHostId,
|
||||||
|
multicast = controller.appPrefs.offerRemoteMulticast.get(),
|
||||||
|
address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_ ?: rh.bindAddress_,
|
||||||
|
port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_
|
||||||
|
)
|
||||||
|
if (r != null) {
|
||||||
|
cachedR.value = r
|
||||||
|
connecting.value = true
|
||||||
|
customAddress.value = cachedR.address
|
||||||
|
customPort.value = cachedR.port
|
||||||
|
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
|
||||||
|
}
|
||||||
|
}
|
||||||
val pairing = remember { chatModel.remoteHostPairing }
|
val pairing = remember { chatModel.remoteHostPairing }
|
||||||
val invitation = rememberSaveable { mutableStateOf<String?>(null) }
|
|
||||||
val port = rememberSaveable { mutableStateOf<String?>(null) }
|
|
||||||
val sessionCode = when (val state = pairing.value?.second) {
|
val sessionCode = when (val state = pairing.value?.second) {
|
||||||
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
|
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
|
||||||
else -> null
|
else -> null
|
||||||
@@ -306,25 +384,25 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
|
|||||||
}
|
}
|
||||||
ConnectMobileViewLayout(
|
ConnectMobileViewLayout(
|
||||||
title = if (cachedSessionCode == null) stringResource(MR.strings.scan_from_mobile) else stringResource(MR.strings.verify_connection),
|
title = if (cachedSessionCode == null) stringResource(MR.strings.scan_from_mobile) else stringResource(MR.strings.verify_connection),
|
||||||
invitation = invitation.value,
|
invitation = cachedR.invitation,
|
||||||
deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName,
|
deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName,
|
||||||
sessionCode = cachedSessionCode,
|
sessionCode = cachedSessionCode,
|
||||||
port = port.value
|
port = cachedR.value?.ctrlPort,
|
||||||
|
staleQrCode = (cachedR.address != customAddress.value && customAddress.value != null) || (cachedR.port != customPort.value && customPort.value != null),
|
||||||
|
editEnabled = !editing && cachedR.addresses.isNotEmpty(),
|
||||||
|
editClicked = { editing = true },
|
||||||
|
refreshQrCode = {
|
||||||
|
withBGApi {
|
||||||
|
if (chatController.stopRemoteHost(rh.remoteHostId)) {
|
||||||
|
startRemoteHost()
|
||||||
|
editing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
UnderQrLayout = { UnderQrLayout(editing, cachedR, customAddress, customPort) }
|
||||||
)
|
)
|
||||||
var remoteHostId by rememberSaveable { mutableStateOf<Long?>(null) }
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
val r = chatModel.controller.startRemoteHost(rh.remoteHostId, controller.appPrefs.offerRemoteMulticast.get())
|
|
||||||
if (r != null) {
|
|
||||||
val (rh_, inv) = r
|
|
||||||
connecting.value = true
|
|
||||||
remoteHostId = rh_?.remoteHostId
|
|
||||||
invitation.value = inv
|
|
||||||
port.value = r.third
|
|
||||||
chatModel.remoteHostPairing.value = null to RemoteHostSessionState.Starting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
|
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
|
||||||
if (remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId) {
|
if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == cachedR.remoteHostId) {
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,10 +412,13 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
|
withBGApi {
|
||||||
|
startRemoteHost()
|
||||||
|
}
|
||||||
onDispose {
|
onDispose {
|
||||||
if (remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != remoteHostId) {
|
if (cachedR.remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != cachedR.remoteHostId) {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
chatController.stopRemoteHost(remoteHostId)
|
chatController.stopRemoteHost(cachedR.remoteHostId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chatModel.remoteHostPairing.value = null
|
chatModel.remoteHostPairing.value = null
|
||||||
@@ -370,3 +451,50 @@ private fun showConnectedMobileDevice(rh: RemoteHostInfo, disconnectHost: () ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UnderQrLayout(editing: Boolean, cachedR: State<CR.RemoteHostStarted?>, customAddress: MutableState<RemoteCtrlAddress?>, customPort: MutableState<Int?>) {
|
||||||
|
if (editing) {
|
||||||
|
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
|
||||||
|
ExposedDropDownSetting(
|
||||||
|
cachedR.addresses.map { it to it.address + " (${it.`interface`})" },
|
||||||
|
customAddress,
|
||||||
|
textColor = MaterialTheme.colors.onBackground,
|
||||||
|
minWidth = 250.dp,
|
||||||
|
maxWidth = with(LocalDensity.current) { 250.sp.toDp() },
|
||||||
|
onSelected = {
|
||||||
|
customAddress.value = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||||
|
mutableStateOf(TextFieldValue((customPort.value ?: cachedR.port!!).toString()))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(DEFAULT_PADDING))
|
||||||
|
DefaultConfigurableTextField(
|
||||||
|
portUnsaved,
|
||||||
|
stringResource(MR.strings.port_verb),
|
||||||
|
modifier = Modifier.widthIn(max = 100.dp),
|
||||||
|
isValid = { validPort(it) && it.toInt() > 1023 },
|
||||||
|
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }),
|
||||||
|
keyboardType = KeyboardType.Number,
|
||||||
|
)
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow { portUnsaved.value.text }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect {
|
||||||
|
if (validPort(it) && it.toInt() > 1023) {
|
||||||
|
customPort.value = it.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val State<CR.RemoteHostStarted?>.rh: RemoteHostInfo? get() = value?.remoteHost_
|
||||||
|
private val State<CR.RemoteHostStarted?>.remoteHostId: Long? get() = value?.remoteHost_?.remoteHostId
|
||||||
|
private val State<CR.RemoteHostStarted?>.invitation: String? get() = value?.invitation
|
||||||
|
private val State<CR.RemoteHostStarted?>.address: RemoteCtrlAddress? get() = value?.localAddrs?.firstOrNull()
|
||||||
|
private val State<CR.RemoteHostStarted?>.addresses: List<RemoteCtrlAddress> get() =
|
||||||
|
(if (controller.appPrefs.developerTools.get()) value?.localAddrs else value?.localAddrs?.filterNot { it.address == "127.0.0.1" }) ?: emptyList()
|
||||||
|
private val State<CR.RemoteHostStarted?>.port: Int? get() = value?.ctrlPort?.toIntOrNull()
|
||||||
|
|||||||
+16
-13
@@ -25,6 +25,7 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.platform.chatModel
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.chat.item.ClickableText
|
import chat.simplex.common.views.chat.item.ClickableText
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
@@ -169,18 +170,20 @@ fun NetworkAndServersView(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
AppBarTitle(stringResource(MR.strings.network_and_servers))
|
AppBarTitle(stringResource(MR.strings.network_and_servers))
|
||||||
SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
|
if (!chatModel.desktopNoUserNoRemote) {
|
||||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) })
|
SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
|
||||||
|
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) })
|
||||||
|
|
||||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) })
|
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) })
|
||||||
|
|
||||||
if (currentRemoteHost == null) {
|
if (currentRemoteHost == null) {
|
||||||
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal)
|
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal)
|
||||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
|
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
|
||||||
if (developerTools) {
|
if (developerTools) {
|
||||||
SessionModePicker(sessionMode, showSettingsModal, updateSessionMode)
|
SessionModePicker(sessionMode, showSettingsModal, updateSessionMode)
|
||||||
|
}
|
||||||
|
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
|
||||||
}
|
}
|
||||||
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentRemoteHost == null && networkUseSocksProxy.value) {
|
if (currentRemoteHost == null && networkUseSocksProxy.value) {
|
||||||
@@ -192,7 +195,7 @@ fun NetworkAndServersView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
||||||
} else {
|
} else if (!chatModel.desktopNoUserNoRemote) {
|
||||||
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +305,7 @@ fun SockProxySettings(m: ChatModel) {
|
|||||||
DefaultConfigurableTextField(
|
DefaultConfigurableTextField(
|
||||||
hostUnsaved,
|
hostUnsaved,
|
||||||
stringResource(MR.strings.host_verb),
|
stringResource(MR.strings.host_verb),
|
||||||
modifier = Modifier,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
isValid = ::validHost,
|
isValid = ::validHost,
|
||||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||||
keyboardType = KeyboardType.Text,
|
keyboardType = KeyboardType.Text,
|
||||||
@@ -312,7 +315,7 @@ fun SockProxySettings(m: ChatModel) {
|
|||||||
DefaultConfigurableTextField(
|
DefaultConfigurableTextField(
|
||||||
portUnsaved,
|
portUnsaved,
|
||||||
stringResource(MR.strings.port_verb),
|
stringResource(MR.strings.port_verb),
|
||||||
modifier = Modifier,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
isValid = ::validPort,
|
isValid = ::validPort,
|
||||||
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
|
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
|
||||||
keyboardType = KeyboardType.Number,
|
keyboardType = KeyboardType.Number,
|
||||||
@@ -425,7 +428,7 @@ private fun validHost(s: String): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://ihateregex.io/expr/port/
|
// https://ihateregex.io/expr/port/
|
||||||
private fun validPort(s: String): Boolean {
|
fun validPort(s: String): Boolean {
|
||||||
val validPort = Regex("^(6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})$")
|
val validPort = Regex("^(6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})$")
|
||||||
return s.isNotBlank() && s.matches(validPort)
|
return s.isNotBlank() && s.matches(validPort)
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-31
@@ -92,7 +92,6 @@ fun PrivacySettingsView(
|
|||||||
chatModel.simplexLinkMode.value = it
|
chatModel.simplexLinkMode.value = it
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SectionDividerSpaced()
|
|
||||||
|
|
||||||
val currentUser = chatModel.currentUser.value
|
val currentUser = chatModel.currentUser.value
|
||||||
if (currentUser != null) {
|
if (currentUser != null) {
|
||||||
@@ -142,39 +141,42 @@ fun PrivacySettingsView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DeliveryReceiptsSection(
|
if (!chatModel.desktopNoUserNoRemote) {
|
||||||
currentUser = currentUser,
|
SectionDividerSpaced()
|
||||||
setOrAskSendReceiptsContacts = { enable ->
|
DeliveryReceiptsSection(
|
||||||
val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat ->
|
currentUser = currentUser,
|
||||||
if (chat.chatInfo is ChatInfo.Direct) {
|
setOrAskSendReceiptsContacts = { enable ->
|
||||||
val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts
|
val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat ->
|
||||||
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
if (chat.chatInfo is ChatInfo.Direct) {
|
||||||
|
val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts
|
||||||
|
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
||||||
|
} else {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contactReceiptsOverrides == 0) {
|
||||||
|
setSendReceiptsContacts(enable, clearOverrides = false)
|
||||||
} else {
|
} else {
|
||||||
count
|
showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setOrAskSendReceiptsGroups = { enable ->
|
||||||
|
val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat ->
|
||||||
|
if (chat.chatInfo is ChatInfo.Group) {
|
||||||
|
val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts
|
||||||
|
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
||||||
|
} else {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (groupReceiptsOverrides == 0) {
|
||||||
|
setSendReceiptsGroups(enable, clearOverrides = false)
|
||||||
|
} else {
|
||||||
|
showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (contactReceiptsOverrides == 0) {
|
)
|
||||||
setSendReceiptsContacts(enable, clearOverrides = false)
|
}
|
||||||
} else {
|
|
||||||
showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setOrAskSendReceiptsGroups = { enable ->
|
|
||||||
val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat ->
|
|
||||||
if (chat.chatInfo is ChatInfo.Group) {
|
|
||||||
val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts
|
|
||||||
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
|
|
||||||
} else {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (groupReceiptsOverrides == 0) {
|
|
||||||
setSendReceiptsGroups(enable, clearOverrides = false)
|
|
||||||
} else {
|
|
||||||
showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
SectionBottomSpacer()
|
SectionBottomSpacer()
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-79
@@ -25,6 +25,7 @@ import androidx.compose.ui.unit.*
|
|||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.views.CreateProfile
|
||||||
import chat.simplex.common.views.database.DatabaseView
|
import chat.simplex.common.views.database.DatabaseView
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.onboarding.SimpleXInfo
|
import chat.simplex.common.views.onboarding.SimpleXInfo
|
||||||
@@ -38,76 +39,39 @@ import kotlinx.coroutines.launch
|
|||||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) {
|
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) {
|
||||||
val user = chatModel.currentUser.value
|
val user = chatModel.currentUser.value
|
||||||
val stopped = chatModel.chatRunning.value == false
|
val stopped = chatModel.chatRunning.value == false
|
||||||
|
SettingsLayout(
|
||||||
if (user != null) {
|
profile = user?.profile,
|
||||||
val requireAuth = remember { chatModel.controller.appPrefs.performLA.state }
|
stopped,
|
||||||
SettingsLayout(
|
chatModel.chatDbEncrypted.value == true,
|
||||||
profile = user.profile,
|
remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value,
|
||||||
stopped,
|
remember { chatModel.controller.appPrefs.notificationsMode.state },
|
||||||
chatModel.chatDbEncrypted.value == true,
|
user?.displayName,
|
||||||
remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value,
|
setPerformLA = setPerformLA,
|
||||||
remember { chatModel.controller.appPrefs.notificationsMode.state },
|
showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } },
|
||||||
user.displayName,
|
showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } },
|
||||||
setPerformLA = setPerformLA,
|
showSettingsModalWithSearch = { modalView ->
|
||||||
showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } },
|
ModalManager.start.showCustomModal { close ->
|
||||||
showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } },
|
val search = rememberSaveable { mutableStateOf("") }
|
||||||
showSettingsModalWithSearch = { modalView ->
|
ModalView(
|
||||||
ModalManager.start.showCustomModal { close ->
|
{ close() },
|
||||||
val search = rememberSaveable { mutableStateOf("") }
|
endButtons = {
|
||||||
ModalView(
|
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
|
||||||
{ close() },
|
},
|
||||||
endButtons = {
|
content = { modalView(chatModel, search) })
|
||||||
SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it }
|
}
|
||||||
},
|
},
|
||||||
content = { modalView(chatModel, search) })
|
showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } },
|
||||||
|
showVersion = {
|
||||||
|
withApi {
|
||||||
|
val info = chatModel.controller.apiGetVersion()
|
||||||
|
if (info != null) {
|
||||||
|
ModalManager.start.showModal { VersionInfoView(info) }
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } },
|
},
|
||||||
showVersion = {
|
withAuth = ::doWithAuth,
|
||||||
withApi {
|
drawerState = drawerState,
|
||||||
val info = chatModel.controller.apiGetVersion()
|
)
|
||||||
if (info != null) {
|
|
||||||
ModalManager.start.showModal { VersionInfoView(info) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
withAuth = { title, desc, block ->
|
|
||||||
if (!requireAuth.value) {
|
|
||||||
block()
|
|
||||||
} else {
|
|
||||||
var autoShow = true
|
|
||||||
ModalManager.fullscreen.showModalCloseable { close ->
|
|
||||||
val onFinishAuth = { success: Boolean ->
|
|
||||||
if (success) {
|
|
||||||
close()
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
if (autoShow) {
|
|
||||||
autoShow = false
|
|
||||||
runAuth(title, desc, onFinishAuth)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
Modifier.fillMaxSize().background(MaterialTheme.colors.background),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
SimpleButton(
|
|
||||||
stringResource(MR.strings.auth_unlock),
|
|
||||||
icon = painterResource(MR.images.ic_lock),
|
|
||||||
click = {
|
|
||||||
runAuth(title, desc, onFinishAuth)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
drawerState = drawerState,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val simplexTeamUri =
|
val simplexTeamUri =
|
||||||
@@ -115,12 +79,12 @@ val simplexTeamUri =
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsLayout(
|
fun SettingsLayout(
|
||||||
profile: LocalProfile,
|
profile: LocalProfile?,
|
||||||
stopped: Boolean,
|
stopped: Boolean,
|
||||||
encrypted: Boolean,
|
encrypted: Boolean,
|
||||||
passphraseSaved: Boolean,
|
passphraseSaved: Boolean,
|
||||||
notificationsMode: State<NotificationsMode>,
|
notificationsMode: State<NotificationsMode>,
|
||||||
userDisplayName: String,
|
userDisplayName: String?,
|
||||||
setPerformLA: (Boolean) -> Unit,
|
setPerformLA: (Boolean) -> Unit,
|
||||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||||
@@ -150,13 +114,22 @@ fun SettingsLayout(
|
|||||||
AppBarTitle(stringResource(MR.strings.your_settings))
|
AppBarTitle(stringResource(MR.strings.your_settings))
|
||||||
|
|
||||||
SectionView(stringResource(MR.strings.settings_section_title_you)) {
|
SectionView(stringResource(MR.strings.settings_section_title_you)) {
|
||||||
SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) {
|
|
||||||
ProfilePreview(profile, stopped = stopped)
|
|
||||||
}
|
|
||||||
val profileHidden = rememberSaveable { mutableStateOf(false) }
|
val profileHidden = rememberSaveable { mutableStateOf(false) }
|
||||||
SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped, extraPadding = true)
|
if (profile != null) {
|
||||||
SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true)
|
SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) {
|
||||||
ChatPreferencesItem(showCustomModal, stopped = stopped)
|
ProfilePreview(profile, stopped = stopped)
|
||||||
|
}
|
||||||
|
SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped, extraPadding = true)
|
||||||
|
SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true)
|
||||||
|
ChatPreferencesItem(showCustomModal, stopped = stopped)
|
||||||
|
} else if (chatModel.localUserCreated.value == false) {
|
||||||
|
SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.create_chat_profile), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.center.showModalCloseable { close ->
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
closeSettings()
|
||||||
|
}
|
||||||
|
CreateProfile(chatModel, close)
|
||||||
|
} } }, disabled = stopped, extraPadding = true)
|
||||||
|
}
|
||||||
if (appPlatform.isDesktop) {
|
if (appPlatform.isDesktop) {
|
||||||
SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped, extraPadding = true)
|
||||||
} else {
|
} else {
|
||||||
@@ -176,10 +149,12 @@ fun SettingsLayout(
|
|||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
|
|
||||||
SectionView(stringResource(MR.strings.settings_section_title_help)) {
|
SectionView(stringResource(MR.strings.settings_section_title_help)) {
|
||||||
SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped, extraPadding = true)
|
||||||
SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped, extraPadding = true)
|
||||||
SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }, extraPadding = true)
|
||||||
SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true)
|
if (!chatModel.desktopNoUserNoRemote) {
|
||||||
|
SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true)
|
||||||
|
}
|
||||||
SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary, extraPadding = true)
|
||||||
}
|
}
|
||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
@@ -469,6 +444,42 @@ fun PreferenceToggleWithIcon(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun doWithAuth(title: String, desc: String, block: () -> Unit) {
|
||||||
|
val requireAuth = chatModel.controller.appPrefs.performLA.get()
|
||||||
|
if (!requireAuth) {
|
||||||
|
block()
|
||||||
|
} else {
|
||||||
|
var autoShow = true
|
||||||
|
ModalManager.fullscreen.showModalCloseable { close ->
|
||||||
|
val onFinishAuth = { success: Boolean ->
|
||||||
|
if (success) {
|
||||||
|
close()
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (autoShow) {
|
||||||
|
autoShow = false
|
||||||
|
runAuth(title, desc, onFinishAuth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
Modifier.fillMaxSize().background(MaterialTheme.colors.background),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
SimpleButton(
|
||||||
|
stringResource(MR.strings.auth_unlock),
|
||||||
|
icon = painterResource(MR.images.ic_lock),
|
||||||
|
click = {
|
||||||
|
runAuth(title, desc, onFinishAuth)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) -> Unit) {
|
private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) -> Unit) {
|
||||||
authenticate(
|
authenticate(
|
||||||
title,
|
title,
|
||||||
|
|||||||
+4
-1
@@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatModel.controller
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.chat.item.ItemAction
|
import chat.simplex.common.views.chat.item.ItemAction
|
||||||
@@ -56,7 +57,9 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
|
|||||||
ModalManager.end.closeModals()
|
ModalManager.end.closeModals()
|
||||||
}
|
}
|
||||||
withBGApi {
|
withBGApi {
|
||||||
m.controller.changeActiveUser(user.remoteHostId, user.userId, userViewPassword(user, searchTextOrPassword.value.trim()))
|
controller.showProgressIfNeeded {
|
||||||
|
m.controller.changeActiveUser(user.remoteHostId, user.userId, userViewPassword(user, searchTextOrPassword.value.trim()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeUser = { user ->
|
removeUser = { user ->
|
||||||
|
|||||||
@@ -565,6 +565,7 @@
|
|||||||
<string name="your_settings">Your settings</string>
|
<string name="your_settings">Your settings</string>
|
||||||
<string name="your_simplex_contact_address">Your SimpleX address</string>
|
<string name="your_simplex_contact_address">Your SimpleX address</string>
|
||||||
<string name="your_chat_profiles">Your chat profiles</string>
|
<string name="your_chat_profiles">Your chat profiles</string>
|
||||||
|
<string name="create_chat_profile">Create chat profile</string>
|
||||||
<string name="database_passphrase_and_export">Database passphrase & export</string>
|
<string name="database_passphrase_and_export">Database passphrase & export</string>
|
||||||
<string name="about_simplex_chat">About SimpleX Chat</string>
|
<string name="about_simplex_chat">About SimpleX Chat</string>
|
||||||
<string name="how_to_use_simplex_chat">How to use it</string>
|
<string name="how_to_use_simplex_chat">How to use it</string>
|
||||||
@@ -1659,6 +1660,7 @@
|
|||||||
<string name="unlink_desktop_question">Unlink desktop?</string>
|
<string name="unlink_desktop_question">Unlink desktop?</string>
|
||||||
<string name="unlink_desktop">Unlink</string>
|
<string name="unlink_desktop">Unlink</string>
|
||||||
<string name="disconnect_remote_host">Disconnect</string>
|
<string name="disconnect_remote_host">Disconnect</string>
|
||||||
|
<string name="disconnect_remote_hosts">Disconnect mobiles</string>
|
||||||
<string name="remote_host_was_disconnected_toast"><![CDATA[Mobile <b>%s</b> was disconnected]]></string>
|
<string name="remote_host_was_disconnected_toast"><![CDATA[Mobile <b>%s</b> was disconnected]]></string>
|
||||||
<string name="disconnect_desktop_question">Disconnect desktop?</string>
|
<string name="disconnect_desktop_question">Disconnect desktop?</string>
|
||||||
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
|
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
|
||||||
@@ -1689,6 +1691,8 @@
|
|||||||
<string name="paste_desktop_address">Paste desktop address</string>
|
<string name="paste_desktop_address">Paste desktop address</string>
|
||||||
<string name="desktop_device">Desktop</string>
|
<string name="desktop_device">Desktop</string>
|
||||||
<string name="not_compatible">Not compatible!</string>
|
<string name="not_compatible">Not compatible!</string>
|
||||||
|
<string name="refresh_qr_code">Refresh</string>
|
||||||
|
<string name="no_connected_mobile">No connected mobile</string>
|
||||||
|
|
||||||
<!-- Under development -->
|
<!-- Under development -->
|
||||||
<string name="in_developing_title">Coming soon!</string>
|
<string name="in_developing_title">Coming soon!</string>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480.433-164.5q-131.583 0-223.758-92.216Q164.5-348.932 164.5-480.082q0-131.149 92.175-223.284Q348.85-795.5 480.5-795.5q84 0 147.75 34.25T738.5-666.5v-129H796V-547H547.5v-57.5H713q-38.032-60.033-96.537-96.767Q557.959-738 480.539-738 372-738 297-663.015q-75 74.986-75 183.25 0 108.265 74.875 183.015Q371.75-222 480.331-222q82.298 0 150.734-47 68.435-47 95.623-124.5H786q-28.5 103-113.49 166-84.991 63-192.077 63Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 516 B |
@@ -15,7 +15,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.window.*
|
import androidx.compose.ui.window.*
|
||||||
import chat.simplex.common.model.ChatController
|
import chat.simplex.common.model.ChatController
|
||||||
import chat.simplex.common.model.ChatModel
|
import chat.simplex.common.model.ChatModel
|
||||||
import chat.simplex.common.platform.desktopPlatform
|
|
||||||
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
|
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
|
||||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
import chat.simplex.common.ui.theme.SimpleXTheme
|
||||||
import chat.simplex.common.views.TerminalView
|
import chat.simplex.common.views.TerminalView
|
||||||
@@ -31,10 +30,30 @@ import java.io.File
|
|||||||
val simplexWindowState = SimplexWindowState()
|
val simplexWindowState = SimplexWindowState()
|
||||||
|
|
||||||
fun showApp() = application {
|
fun showApp() = application {
|
||||||
// For some reason on Linux actual width will be 10.dp less after specifying it here. If we specify 1366,
|
// Creates file if not exists; comes with proper defaults
|
||||||
// it will show 1356. But after that we can still update it to 1366 by changing window state. Just making it +10 now here
|
val state = getStoredWindowState()
|
||||||
val width = if (desktopPlatform.isLinux()) 1376.dp else 1366.dp
|
|
||||||
val windowState = rememberWindowState(placement = WindowPlacement.Floating, width = width, height = 768.dp)
|
val windowState: WindowState = rememberWindowState(
|
||||||
|
placement = WindowPlacement.Floating,
|
||||||
|
width = state.width.dp,
|
||||||
|
height = state.height.dp,
|
||||||
|
position = WindowPosition(state.x.dp, state.y.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(
|
||||||
|
windowState.position.x.value,
|
||||||
|
windowState.position.y.value,
|
||||||
|
windowState.size.width.value,
|
||||||
|
windowState.size.height.value
|
||||||
|
) {
|
||||||
|
storeWindowState(WindowPositionSize(
|
||||||
|
x = windowState.position.x.value.toInt(),
|
||||||
|
y = windowState.position.y.value.toInt(),
|
||||||
|
width = windowState.size.width.value.toInt(),
|
||||||
|
height = windowState.size.height.value.toInt()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
simplexWindowState.windowState = windowState
|
simplexWindowState.windowState = windowState
|
||||||
// Reload all strings in all @Composable's after language change at runtime
|
// Reload all strings in all @Composable's after language change at runtime
|
||||||
if (remember { ChatController.appPrefs.appLanguage.state }.value != "") {
|
if (remember { ChatController.appPrefs.appLanguage.state }.value != "") {
|
||||||
@@ -124,7 +143,7 @@ fun showApp() = application {
|
|||||||
var hiddenUntilRestart by remember { mutableStateOf(false) }
|
var hiddenUntilRestart by remember { mutableStateOf(false) }
|
||||||
if (!hiddenUntilRestart) {
|
if (!hiddenUntilRestart) {
|
||||||
val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH, height = 768.dp)
|
val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH, height = 768.dp)
|
||||||
Window(state = cWindowState, onCloseRequest = ::exitApplication, title = stringResource(MR.strings.chat_console)) {
|
Window(state = cWindowState, onCloseRequest = { hiddenUntilRestart = true }, title = stringResource(MR.strings.chat_console)) {
|
||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
TerminalView(ChatModel) { hiddenUntilRestart = true }
|
TerminalView(ChatModel) { hiddenUntilRestart = true }
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package chat.simplex.common
|
||||||
|
|
||||||
|
import chat.simplex.common.model.json
|
||||||
|
import chat.simplex.common.platform.appPreferences
|
||||||
|
import chat.simplex.common.platform.desktopPlatform
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class WindowPositionSize(
|
||||||
|
val width: Int = 1366,
|
||||||
|
val height: Int = 768,
|
||||||
|
val x: Int = 0,
|
||||||
|
val y: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getStoredWindowState(): WindowPositionSize =
|
||||||
|
try {
|
||||||
|
val str = appPreferences.desktopWindowState.get()
|
||||||
|
var state = if (str == null) {
|
||||||
|
WindowPositionSize()
|
||||||
|
} else {
|
||||||
|
json.decodeFromString(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For some reason on Linux actual width will be 10.dp less after specifying it here. If we specify 1366,
|
||||||
|
// it will show 1356. But after that we can still update it to 1366 by changing window state. Just making it +10 now here
|
||||||
|
if (desktopPlatform.isLinux() && state.width == 1366) {
|
||||||
|
state = state.copy(width = 1376)
|
||||||
|
}
|
||||||
|
state
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
WindowPositionSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun storeWindowState(state: WindowPositionSize) =
|
||||||
|
appPreferences.desktopWindowState.set(json.encodeToString(state))
|
||||||
-1
@@ -136,7 +136,6 @@ private fun SendStateUpdates() {
|
|||||||
.collect { call ->
|
.collect { call ->
|
||||||
val state = call.callState.text
|
val state = call.callState.text
|
||||||
val connInfo = call.connectionInfo
|
val connInfo = call.connectionInfo
|
||||||
// val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})"
|
|
||||||
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
|
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
|
||||||
val description = call.encryptionStatus + connInfoText
|
val description = call.encryptionStatus + connInfoText
|
||||||
chatModel.callCommand.add(WCallCommand.Description(state, description))
|
chatModel.callCommand.add(WCallCommand.Description(state, description))
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package chat.simplex.common.views.onboarding
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import chat.simplex.common.model.ChatModel.controller
|
||||||
|
import chat.simplex.common.model.SharedPreference
|
||||||
|
import chat.simplex.common.model.User
|
||||||
|
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)?) {
|
||||||
|
if (user == null) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING * 2.5f)) {
|
||||||
|
OnboardingActionButton(MR.strings.link_a_mobile, onboarding = if (controller.appPrefs.initialRandomDBPassphrase.get()) OnboardingStage.Step2_5_SetupDatabasePassphrase else OnboardingStage.LinkAMobile, true, icon = painterResource(MR.images.ic_smartphone_300), onclick = onclick)
|
||||||
|
OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, icon = painterResource(MR.images.ic_desktop), onclick = onclick)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick = onclick)
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-4
@@ -2,14 +2,16 @@ packages: .
|
|||||||
-- packages: . ../simplexmq
|
-- packages: . ../simplexmq
|
||||||
-- packages: . ../simplexmq ../direct-sqlcipher ../sqlcipher-simple
|
-- packages: . ../simplexmq ../direct-sqlcipher ../sqlcipher-simple
|
||||||
|
|
||||||
with-compiler: ghc-9.6.3
|
with-compiler: ghc-8.10.7
|
||||||
|
|
||||||
|
index-state: 2023-10-06T00:00:00Z
|
||||||
|
|
||||||
constraints: zip +disable-bzip2 +disable-zstd
|
constraints: zip +disable-bzip2 +disable-zstd
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/simplex-chat/simplexmq.git
|
location: https://github.com/simplex-chat/simplexmq.git
|
||||||
tag: 757b7eec81341d8560a326deab303bb6fb6a26a3
|
tag: 140b4b12bdee809bcb5484101b5f1b753c744b51
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
@@ -46,7 +48,8 @@ source-repository-package
|
|||||||
location: https://github.com/simplex-chat/android-support.git
|
location: https://github.com/simplex-chat/android-support.git
|
||||||
tag: 9aa09f148089d6752ce563b14c2df1895718d806
|
tag: 9aa09f148089d6752ce563b14c2df1895718d806
|
||||||
|
|
||||||
|
-- TODO this fork is only needed to compile with GHC 8.10.7 - it allows previous base version
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/simplex-chat/network-transport.git
|
location: https://github.com/simplex-chat/zip.git
|
||||||
tag: 0013798272a683e35ca38d2fdaf480942311fba8
|
tag: bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
active-repositories: hackage.haskell.org:merge
|
||||||
|
constraints: any.Cabal ==3.10.1.0,
|
||||||
|
any.Cabal-syntax ==3.10.1.0,
|
||||||
|
any.HUnit ==1.6.2.0,
|
||||||
|
any.OneTuple ==0.4.1.1,
|
||||||
|
any.Only ==0.1,
|
||||||
|
any.QuickCheck ==2.14.3,
|
||||||
|
QuickCheck -old-random +templatehaskell,
|
||||||
|
any.SHA ==1.6.4.4,
|
||||||
|
SHA -exe,
|
||||||
|
any.StateVar ==1.2.2,
|
||||||
|
any.aeson ==2.1.2.1,
|
||||||
|
aeson +ordered-keymap,
|
||||||
|
any.android-support ==0.1.0.0,
|
||||||
|
any.ansi-terminal ==0.11.5,
|
||||||
|
ansi-terminal -example +win32-2-13-1,
|
||||||
|
any.ansi-terminal-types ==0.11.5,
|
||||||
|
any.ansi-wl-pprint ==0.6.9,
|
||||||
|
ansi-wl-pprint -example,
|
||||||
|
any.appar ==0.1.8,
|
||||||
|
any.array ==0.5.4.0,
|
||||||
|
any.asn1-encoding ==0.9.6,
|
||||||
|
any.asn1-parse ==0.9.5,
|
||||||
|
any.asn1-types ==0.3.4,
|
||||||
|
any.assoc ==1.1,
|
||||||
|
assoc +tagged,
|
||||||
|
any.async ==2.2.4,
|
||||||
|
async -bench,
|
||||||
|
any.attoparsec ==0.14.4,
|
||||||
|
attoparsec -developer,
|
||||||
|
any.auto-update ==0.1.6,
|
||||||
|
any.base ==4.14.3.0,
|
||||||
|
any.base-orphans ==0.9.0,
|
||||||
|
any.base64-bytestring ==1.2.1.0,
|
||||||
|
any.basement ==0.0.16,
|
||||||
|
any.bifunctors ==5.6.1,
|
||||||
|
bifunctors +tagged,
|
||||||
|
any.binary ==0.8.9.1,
|
||||||
|
any.bitvec ==1.1.5.0,
|
||||||
|
bitvec +simd,
|
||||||
|
any.blaze-builder ==0.4.2.3,
|
||||||
|
any.blaze-textual ==0.2.3.1,
|
||||||
|
blaze-textual -developer -integer-simple +native,
|
||||||
|
any.byteorder ==1.0.4,
|
||||||
|
any.bytestring ==0.11.5.2,
|
||||||
|
any.bytestring-builder ==0.10.8.2.0,
|
||||||
|
bytestring-builder +bytestring_has_builder,
|
||||||
|
any.call-stack ==0.4.0,
|
||||||
|
any.case-insensitive ==1.2.1.0,
|
||||||
|
any.cereal ==0.5.8.3,
|
||||||
|
cereal -bytestring-builder,
|
||||||
|
any.clock ==0.8.4,
|
||||||
|
clock -llvm,
|
||||||
|
any.colour ==2.3.6,
|
||||||
|
any.comonad ==5.0.8,
|
||||||
|
comonad +containers +distributive +indexed-traversable,
|
||||||
|
any.composition ==1.0.2.2,
|
||||||
|
any.conduit ==1.3.5,
|
||||||
|
any.conduit-extra ==1.3.6,
|
||||||
|
any.constraints ==0.13.4,
|
||||||
|
any.containers ==0.6.5.1,
|
||||||
|
any.contravariant ==1.5.5,
|
||||||
|
contravariant +semigroups +statevar +tagged,
|
||||||
|
any.cryptonite ==0.30,
|
||||||
|
cryptonite -check_alignment +integer-gmp -old_toolchain_inliner +support_aesni +support_deepseq -support_pclmuldq +support_rdrand -support_sse +use_target_attributes,
|
||||||
|
any.cryptostore ==0.2.3.0,
|
||||||
|
any.data-array-byte ==0.1.0.1,
|
||||||
|
any.data-default ==0.7.1.1,
|
||||||
|
any.data-default-class ==0.1.2.0,
|
||||||
|
any.data-default-instances-containers ==0.0.1,
|
||||||
|
any.data-default-instances-dlist ==0.0.1,
|
||||||
|
any.data-default-instances-old-locale ==0.0.1,
|
||||||
|
any.data-fix ==0.3.2,
|
||||||
|
any.deepseq ==1.4.4.0,
|
||||||
|
any.digest ==0.0.1.7,
|
||||||
|
digest +pkg-config,
|
||||||
|
any.direct-sqlcipher ==2.3.28,
|
||||||
|
direct-sqlcipher -commoncrypto +fulltextsearch +haveusleep +json1 -mathfunctions -openssl -systemlib -tomcrypt +urifilenames,
|
||||||
|
any.directory ==1.3.7.1,
|
||||||
|
any.distributive ==0.6.2.1,
|
||||||
|
distributive +semigroups +tagged,
|
||||||
|
any.dlist ==1.0,
|
||||||
|
dlist -werror,
|
||||||
|
any.easy-file ==0.2.5,
|
||||||
|
any.email-validate ==2.3.2.19,
|
||||||
|
any.entropy ==0.4.1.10,
|
||||||
|
entropy -donotgetentropy,
|
||||||
|
any.exceptions ==0.10.7,
|
||||||
|
exceptions +transformers-0-4,
|
||||||
|
any.fast-logger ==3.2.2,
|
||||||
|
any.filepath ==1.4.2.1,
|
||||||
|
any.foldable1-classes-compat ==0.1,
|
||||||
|
foldable1-classes-compat +tagged,
|
||||||
|
any.generic-random ==1.5.0.1,
|
||||||
|
generic-random -enable-inspect,
|
||||||
|
any.generically ==0.1.1,
|
||||||
|
any.ghc-boot-th ==8.10.7,
|
||||||
|
any.ghc-prim ==0.6.1,
|
||||||
|
any.hashable ==1.4.3.0,
|
||||||
|
hashable +integer-gmp -random-initial-seed,
|
||||||
|
any.hourglass ==0.2.12,
|
||||||
|
any.hsc2hs ==0.68.10,
|
||||||
|
hsc2hs -in-ghc-tree,
|
||||||
|
any.hspec ==2.7.10,
|
||||||
|
any.hspec-core ==2.7.10,
|
||||||
|
any.hspec-discover ==2.7.10,
|
||||||
|
any.hspec-expectations ==0.8.2,
|
||||||
|
any.http-types ==0.12.3,
|
||||||
|
any.http2 ==4.2.2,
|
||||||
|
http2 -devel -h2spec,
|
||||||
|
any.indexed-traversable ==0.1.3,
|
||||||
|
any.indexed-traversable-instances ==0.1.1.2,
|
||||||
|
any.ini ==0.4.1,
|
||||||
|
any.integer-conversion ==0.1.0.1,
|
||||||
|
any.integer-gmp ==1.0.3.0,
|
||||||
|
any.integer-logarithms ==1.0.3.1,
|
||||||
|
integer-logarithms -check-bounds +integer-gmp,
|
||||||
|
any.iproute ==1.7.12,
|
||||||
|
any.iso8601-time ==0.1.5,
|
||||||
|
iso8601-time +new-time,
|
||||||
|
any.libyaml ==0.1.2,
|
||||||
|
libyaml -no-unicode -system-libyaml,
|
||||||
|
any.lifted-base ==0.2.3.12,
|
||||||
|
any.memory ==0.18.0,
|
||||||
|
memory +support_bytestring +support_deepseq,
|
||||||
|
any.monad-control ==1.0.3.1,
|
||||||
|
any.monad-logger ==0.3.40,
|
||||||
|
monad-logger +template_haskell,
|
||||||
|
any.monad-loops ==0.4.3,
|
||||||
|
monad-loops +base4,
|
||||||
|
any.mono-traversable ==1.0.15.3,
|
||||||
|
any.mtl ==2.3.1,
|
||||||
|
any.network ==3.1.4.0,
|
||||||
|
network -devel,
|
||||||
|
any.network-byte-order ==0.1.6,
|
||||||
|
any.network-info ==0.2.1,
|
||||||
|
any.network-transport ==0.5.6,
|
||||||
|
any.network-udp ==0.0.0,
|
||||||
|
any.network-uri ==2.6.4.2,
|
||||||
|
any.old-locale ==1.0.0.7,
|
||||||
|
any.old-time ==1.1.0.3,
|
||||||
|
any.optparse-applicative ==0.16.1.0,
|
||||||
|
optparse-applicative +process,
|
||||||
|
any.parsec ==3.1.17.0,
|
||||||
|
any.pem ==0.2.4,
|
||||||
|
any.pretty ==1.1.3.6,
|
||||||
|
any.prettyprinter ==1.7.1,
|
||||||
|
prettyprinter -buildreadme +text,
|
||||||
|
any.primitive ==0.8.0.0,
|
||||||
|
any.process ==1.6.18.0,
|
||||||
|
any.psqueues ==0.2.7.3,
|
||||||
|
any.quickcheck-io ==0.2.0,
|
||||||
|
any.random ==1.2.1.1,
|
||||||
|
any.record-hasfield ==1.0,
|
||||||
|
any.resourcet ==1.3.0,
|
||||||
|
any.rts ==1.0.1,
|
||||||
|
any.safe-exceptions ==0.1.7.4,
|
||||||
|
any.scientific ==0.3.7.0,
|
||||||
|
scientific -bytestring-builder -integer-simple,
|
||||||
|
any.semialign ==1.3,
|
||||||
|
semialign +semigroupoids,
|
||||||
|
any.semigroupoids ==6.0.0.1,
|
||||||
|
semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers,
|
||||||
|
any.setenv ==0.1.1.3,
|
||||||
|
any.silently ==1.2.5.3,
|
||||||
|
any.simple-logger ==0.1.1,
|
||||||
|
simplex-chat -swift,
|
||||||
|
any.simplexmq ==5.4.0.7,
|
||||||
|
simplexmq -swift,
|
||||||
|
any.socks ==0.6.1,
|
||||||
|
any.split ==0.2.3.5,
|
||||||
|
any.splitmix ==0.1.0.5,
|
||||||
|
splitmix -optimised-mixer,
|
||||||
|
any.sqlcipher-simple ==0.4.18.1,
|
||||||
|
any.stm ==2.5.0.1,
|
||||||
|
any.stm-chans ==3.0.0.9,
|
||||||
|
any.streaming-commons ==0.2.2.6,
|
||||||
|
streaming-commons -use-bytestring-builder,
|
||||||
|
any.strict ==0.5,
|
||||||
|
any.tagged ==0.8.8,
|
||||||
|
tagged +deepseq +transformers,
|
||||||
|
any.template-haskell ==2.16.0.0,
|
||||||
|
any.temporary ==1.3,
|
||||||
|
any.terminal ==0.2.0.0,
|
||||||
|
any.text ==2.0,
|
||||||
|
text -developer +simdutf,
|
||||||
|
any.text-iso8601 ==0.1,
|
||||||
|
any.text-short ==0.1.5,
|
||||||
|
text-short -asserts,
|
||||||
|
any.tf-random ==0.5,
|
||||||
|
any.th-abstraction ==0.6.0.0,
|
||||||
|
any.th-compat ==0.1.4,
|
||||||
|
any.these ==1.2,
|
||||||
|
any.time ==1.9.3,
|
||||||
|
any.time-compat ==1.9.6.1,
|
||||||
|
time-compat -old-locale,
|
||||||
|
any.time-manager ==0.0.1,
|
||||||
|
any.tls ==1.6.0,
|
||||||
|
tls +compat -hans +network,
|
||||||
|
any.transformers ==0.6.1.1,
|
||||||
|
any.transformers-base ==0.4.6,
|
||||||
|
transformers-base +orphaninstances,
|
||||||
|
any.transformers-compat ==0.7.2,
|
||||||
|
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
|
||||||
|
any.type-equality ==1,
|
||||||
|
any.typed-process ==0.2.11.0,
|
||||||
|
any.unix ==2.7.3,
|
||||||
|
any.unix-compat ==0.7,
|
||||||
|
unix-compat -old-time,
|
||||||
|
any.unix-time ==0.4.11,
|
||||||
|
any.unliftio ==0.2.25.0,
|
||||||
|
any.unliftio-core ==0.2.1.0,
|
||||||
|
any.unordered-containers ==0.2.19.1,
|
||||||
|
unordered-containers -debug,
|
||||||
|
any.uuid-types ==1.0.5.1,
|
||||||
|
any.vector ==0.13.0.0,
|
||||||
|
vector +boundschecks -internalchecks -unsafechecks -wall,
|
||||||
|
any.vector-algorithms ==0.9.0.1,
|
||||||
|
vector-algorithms +bench +boundschecks -internalchecks -llvm +properties -unsafechecks,
|
||||||
|
any.vector-stream ==0.1.0.0,
|
||||||
|
any.websockets ==0.12.7.3,
|
||||||
|
websockets -example,
|
||||||
|
any.witherable ==0.4.2,
|
||||||
|
any.x509 ==1.7.7,
|
||||||
|
any.x509-store ==1.6.9,
|
||||||
|
any.x509-validation ==1.6.12,
|
||||||
|
any.yaml ==0.11.11.2,
|
||||||
|
yaml +no-examples +no-exe,
|
||||||
|
any.zip ==2.0.0,
|
||||||
|
zip -dev +disable-bzip2 +disable-zstd,
|
||||||
|
any.zlib ==0.6.3.0,
|
||||||
|
zlib -bundled-c-zlib -non-blocking-ffi -pkg-config
|
||||||
|
index-state: hackage.haskell.org 2023-10-05T21:19:55Z
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Inactive group members
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Group traffic is higher than necessary due to lack of diagnosis of inactive group members. By inactive we understand group members who went offline for indefinitely long time, uninstalled application without leaving group, failed to send x.grp.leave message before deleting connection, or in any other way failed to explicitly communicate further inactivity.
|
||||||
|
|
||||||
|
Currently other group members continue to identify such members as active and to send messages to their connections until exceeding receiving SMP queues quotas, with pending messages being slowly retried even after that.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Identify inactive members and don't send messages to their connections. Silent periodically online members should continue to receive messages, so decision to mark member as inactive should be made conservatively.
|
||||||
|
|
||||||
|
Agent:
|
||||||
|
- on SMP.QUOTA error notify client with ERR CONN QUOTA (new ConnectionErrorType QUOTA).
|
||||||
|
- on receiving QCONT notify client (new event).
|
||||||
|
|
||||||
|
Chat, on sending side, per member:
|
||||||
|
- unanswered_snd_msg_count - number of messages that were sent consecutively without receiving a message from member.
|
||||||
|
- last_rcv_ts - timestamp of last received message.
|
||||||
|
- inactive flag.
|
||||||
|
- set inactive if:
|
||||||
|
- agent reports QUOTA error.
|
||||||
|
- on sending message: (unanswered_snd_msg_count > K) && (last_rcv_ts earlier than Ddiff days ago), Ddiff = 1/2/3 days?
|
||||||
|
- reset inactive:
|
||||||
|
- on receiving QCONT.
|
||||||
|
- on receiving message or receipt. Also reset unanswered_snd_msg_count, last_rcv_ts.
|
||||||
|
- don't send to member if inactive.
|
||||||
|
- don't send only content messages (x.msg.new, etc.) and always send messages altering group state?
|
||||||
|
- unanswered_snd_msg_count, last_rcv_ts to be tracked, checked, reset only for members with compatible version.
|
||||||
|
|
||||||
|
Chat, on receiving side, per member:
|
||||||
|
- unanswered_rcv_msg_count - number of messages that were received consecutively without sending a message to member.
|
||||||
|
- send non-optional receipt / another (new) protocol message if:
|
||||||
|
- on receiving message: unanswered_rcv_msg_count > M, M < K.
|
||||||
|
- on sending a message or receipt to member reset unanswered_rcv_msg_count.
|
||||||
|
- unanswered_rcv_msg_count to be tracked, checked, reset only for members with compatible version.
|
||||||
|
|
||||||
|
\***
|
||||||
|
|
||||||
|
Consider above condition:
|
||||||
|
|
||||||
|
> (unanswered_snd_msg_count > K) && (last_rcv_ts earlier than Ddiff days ago)
|
||||||
|
|
||||||
|
It still doesn't account for following situation:
|
||||||
|
|
||||||
|
1. Sending member sends a few (N1, N1 < M) messages to silent member on day D1.
|
||||||
|
2. Sending member doesn't send messages for several days.
|
||||||
|
3. Sending member sends more messages (N2, N1 + N2 > K) to silent member on day DI (DI - D1 > diff in days in above condition), while silent member is offline.
|
||||||
|
- Sending member checks above condition and evaluates it to be true, marks silent member as inactive.
|
||||||
|
- Simply remembering last_snd_ts on sending side and adding check for it not being from several days ago to above condition is not enough, as it will be overwritten by current day sends and will only evaluate false for the first send. What could work is remembering prev_session_last_snd_ts or prev_day_last_snd_ts, but it further complicates logic, and still probably wouldn't account for some time zone differences.
|
||||||
|
4. Sending member sends yet more messages, which will not be queued for silent member marked inactive.
|
||||||
|
5. Silent member comes online, sends receipt upon receiving message fulfilling above condition: `unanswered_rcv_msg_count > M`, and will lose following messages.
|
||||||
|
- If sending member created messages from 4 as pending, and sent them upon receiving receipt from silent member, silent member would only receive them after sending member coming online. If they are in different time zones it may happen on next day.
|
||||||
|
|
||||||
|
Same situation can occur even without step 1, simply by sending many messages while other member is offline.
|
||||||
|
|
||||||
|
The problem is less acute the greater the difference between K and M, but making K >> M renders this whole mechanism obsolete, as we could then simply rely on QUOTA errors to mark group members inactive (and don't slow retry in agent?).
|
||||||
|
|
||||||
|
Perhaps an acceptable way to solve this problem is to add a task to cleanup manager that would send receipts to all members on condition: (unanswered_rcv_msg_count > 0) && (last_reply_ts earlier than 1 day ago). (Adds last_reply_ts to tracking on receiving side). Perhaps it should be a task separate from cleanup manager that only occurs once per start, or with longer interval.
|
||||||
|
|
||||||
|
\***
|
||||||
|
|
||||||
|
Additionally we could consider group member connection as disabled with smaller AUTH error count. Currently it's 10 messages, could be 1.
|
||||||
|
|
||||||
|
### Delivery suspension notice
|
||||||
|
|
||||||
|
When receiving side comes back online, replies and continues to receive messages, it has no way of knowing there was a gap in messages from sending member. To notify receiving member about delivery suspension, sending member should send notice containing shared message id of the last sent message (new protocol event) to them:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
XGrpMemSuspended :: SharedMsgId -> ChatMsgEvent 'Json
|
||||||
|
```
|
||||||
|
|
||||||
|
Sending side additionally tracks:
|
||||||
|
- xgrpmemsuspended_sent flag - to only send it once.
|
||||||
|
|
||||||
|
When processing it, receiving member creates a "gap" chat item (e.g. event saying "member x suspended delivery to you due to your inactivity, there may be a gap in messages").
|
||||||
|
|
||||||
|
After receiving member signals activity by sending any reply, sending member may send message history before continuing normal delivery.
|
||||||
|
|
||||||
|
Starting point for message history: either receiving member could request history starting from specific shared message id (received in XGrpMemSuspended) with another new protocol event, or sending member can remember it instead of just flag.
|
||||||
|
|
||||||
|
### Sending message history
|
||||||
|
|
||||||
|
New protocol event:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
XGrpMsgHistory :: [ChatMessage 'Json] -> ChatMsgEvent 'Json
|
||||||
|
```
|
||||||
|
|
||||||
|
Sending member builds messages history starting starting from requested/remembered shared message id:
|
||||||
|
- `messages` table is periodically cleaned up, so messages would be retrieved from `chat_items`.
|
||||||
|
- if chat item for starting shared message id is not found (it may have been deleted manually or as a disappearing message), abort?
|
||||||
|
- sending member could track number of skipped messages per member, but again if any chat items were deleted, older (previously successfully sent) chat items would be retrieved, resulting in duplicate messages. If receiving member has also cleaned up records in `messages` table, they wouldn't be deduplicated.
|
||||||
|
- sending member could track timestamp of first unsent message instead of shared msg id.
|
||||||
|
- sending member should probably limit maximum number of messages sent as history (100?).
|
||||||
|
- only XMsgNew events should be sent in XGrpMsgHistory (chat items to be transformed back into text messages).
|
||||||
|
- updates, deletions would be reflected in chat item list.
|
||||||
|
- reactions would be omitted.
|
||||||
|
- files would be likely expired by the time of sending history, so only file name and size may be sent in FileInvitation, with invitation being practically not acceptable.
|
||||||
|
- add new flag to CIFile "expired" for receiving member to mark chat items created based on such invitations.
|
||||||
|
- FileInvitation in MsgContainer could also contain this flag as optional to explicitly communicate that only file metadata is sent.
|
||||||
|
- alternatively sending member could re-upload files, but this seems excessive.
|
||||||
|
- XMsgNew events don't include message timestamps (instead usually broker ts is retrieved from agent message meta), so receiving member wouldn't be able to restore them from history. Perhaps history should include XGrpMsgForward events containing XMsgNew events instead.
|
||||||
|
- XGrpMsgHistory is likely to exceed message block limit.
|
||||||
|
- either multiple messages comprising a history can be batched as a single message on chat level until the block size is exceeded.
|
||||||
|
- or large history messages could be batched on agent level.
|
||||||
|
|
||||||
|
\***
|
||||||
|
|
||||||
|
Same XGrpMsgHistory protocol event could be sent by host to new members, after sending introductions.
|
||||||
Generated
+473
-214
@@ -16,6 +16,21 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"blank": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1625557891,
|
||||||
|
"narHash": "sha256-O8/MWsPBGhhyPoPLHZAuoZiiHo9q6FLlEeIDEXuj6T4=",
|
||||||
|
"owner": "divnix",
|
||||||
|
"repo": "blank",
|
||||||
|
"rev": "5a5d2684073d9f563072ed07c871d577a6c614a8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "divnix",
|
||||||
|
"repo": "blank",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"cabal-32": {
|
"cabal-32": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
@@ -83,6 +98,64 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"devshell": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1663445644,
|
||||||
|
"narHash": "sha256-+xVlcK60x7VY1vRJbNUEAHi17ZuoQxAIH4S4iUFUGBA=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "devshell",
|
||||||
|
"rev": "e3dc3e21594fe07bdb24bdf1c8657acaa4cb8f66",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "devshell",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dmerge": {
|
||||||
|
"inputs": {
|
||||||
|
"nixlib": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"yants": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"yants"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1659548052,
|
||||||
|
"narHash": "sha256-fzI2gp1skGA8mQo/FBFrUAtY0GQkAIAaV/V127TJPyY=",
|
||||||
|
"owner": "divnix",
|
||||||
|
"repo": "data-merge",
|
||||||
|
"rev": "d160d18ce7b1a45b88344aa3f13ed1163954b497",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "divnix",
|
||||||
|
"repo": "data-merge",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
@@ -100,21 +173,19 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-parts": {
|
"flake-compat_2": {
|
||||||
"inputs": {
|
"flake": false,
|
||||||
"nixpkgs-lib": "nixpkgs-lib"
|
|
||||||
},
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1698579227,
|
"lastModified": 1650374568,
|
||||||
"narHash": "sha256-KVWjFZky+gRuWennKsbo6cWyo7c/z/VgCte5pR9pEKg=",
|
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
|
||||||
"owner": "hercules-ci",
|
"owner": "edolstra",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-compat",
|
||||||
"rev": "f76e870d64779109e41370848074ac4eaa1606ec",
|
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "hercules-ci",
|
"owner": "edolstra",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-compat",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -133,6 +204,51 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"flake-utils_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1667395993,
|
||||||
|
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils_3": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1653893745,
|
||||||
|
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils_4": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1659877975,
|
||||||
|
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ghc-8.6.5-iohk": {
|
"ghc-8.6.5-iohk": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
@@ -150,51 +266,33 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ghc98X": {
|
"gomod2nix": {
|
||||||
"flake": false,
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs_2",
|
||||||
|
"utils": "utils"
|
||||||
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1696643148,
|
"lastModified": 1655245309,
|
||||||
"narHash": "sha256-E02DfgISH7EvvNAu0BHiPvl1E5FGMDi0pWdNZtIBC9I=",
|
"narHash": "sha256-d/YPoQ/vFn1+GTmSdvbSBSTOai61FONxB4+Lt6w/IVI=",
|
||||||
"ref": "ghc-9.8",
|
"owner": "tweag",
|
||||||
"rev": "443e870d977b1ab6fc05f47a9a17bc49296adbd6",
|
"repo": "gomod2nix",
|
||||||
"revCount": 61642,
|
"rev": "40d32f82fc60d66402eb0972e6e368aeab3faf58",
|
||||||
"submodules": true,
|
"type": "github"
|
||||||
"type": "git",
|
|
||||||
"url": "https://gitlab.haskell.org/ghc/ghc"
|
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"ref": "ghc-9.8",
|
"owner": "tweag",
|
||||||
"submodules": true,
|
"repo": "gomod2nix",
|
||||||
"type": "git",
|
"type": "github"
|
||||||
"url": "https://gitlab.haskell.org/ghc/ghc"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ghc99": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1697054644,
|
|
||||||
"narHash": "sha256-kKarOuXUaAH3QWv7ASx+gGFMHaHKe0pK5Zu37ky2AL4=",
|
|
||||||
"ref": "refs/heads/master",
|
|
||||||
"rev": "f383a242c76f90bcca8a4d7ee001dcb49c172a9a",
|
|
||||||
"revCount": 62040,
|
|
||||||
"submodules": true,
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://gitlab.haskell.org/ghc/ghc"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"submodules": true,
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://gitlab.haskell.org/ghc/ghc"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hackage": {
|
"hackage": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1699834964,
|
"lastModified": 1696724662,
|
||||||
"narHash": "sha256-733KT+G0c1euCeb60/u1qbX22Kvu9lNnIDfAmk6Jxq0=",
|
"narHash": "sha256-jV2ugSjZE0FjMYR2YIx0p2cDBqd+xxhZrRxp5BmieYk=",
|
||||||
"owner": "input-output-hk",
|
"owner": "input-output-hk",
|
||||||
"repo": "hackage.nix",
|
"repo": "hackage.nix",
|
||||||
"rev": "2e891e530400187ea1083ffef15adf259061be41",
|
"rev": "df603bff8606d8653a0876ae0c3fd1f9014882f2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -211,40 +309,33 @@
|
|||||||
"cabal-36": "cabal-36",
|
"cabal-36": "cabal-36",
|
||||||
"cardano-shell": "cardano-shell",
|
"cardano-shell": "cardano-shell",
|
||||||
"flake-compat": "flake-compat",
|
"flake-compat": "flake-compat",
|
||||||
|
"flake-utils": "flake-utils_2",
|
||||||
"ghc-8.6.5-iohk": "ghc-8.6.5-iohk",
|
"ghc-8.6.5-iohk": "ghc-8.6.5-iohk",
|
||||||
"ghc98X": "ghc98X",
|
|
||||||
"ghc99": "ghc99",
|
|
||||||
"hackage": [
|
"hackage": [
|
||||||
"hackage"
|
"hackage"
|
||||||
],
|
],
|
||||||
"hls-1.10": "hls-1.10",
|
|
||||||
"hls-2.0": "hls-2.0",
|
|
||||||
"hls-2.2": "hls-2.2",
|
|
||||||
"hls-2.3": "hls-2.3",
|
|
||||||
"hls-2.4": "hls-2.4",
|
|
||||||
"hpc-coveralls": "hpc-coveralls",
|
"hpc-coveralls": "hpc-coveralls",
|
||||||
"hydra": "hydra",
|
"hydra": "hydra",
|
||||||
"iserv-proxy": "iserv-proxy",
|
"iserv-proxy": "iserv-proxy",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"haskellNix",
|
"nixpkgs"
|
||||||
"nixpkgs-unstable"
|
|
||||||
],
|
],
|
||||||
"nixpkgs-2003": "nixpkgs-2003",
|
"nixpkgs-2003": "nixpkgs-2003",
|
||||||
"nixpkgs-2105": "nixpkgs-2105",
|
"nixpkgs-2105": "nixpkgs-2105",
|
||||||
"nixpkgs-2111": "nixpkgs-2111",
|
"nixpkgs-2111": "nixpkgs-2111",
|
||||||
"nixpkgs-2205": "nixpkgs-2205",
|
"nixpkgs-2205": "nixpkgs-2205",
|
||||||
"nixpkgs-2211": "nixpkgs-2211",
|
"nixpkgs-2211": "nixpkgs-2211",
|
||||||
"nixpkgs-2305": "nixpkgs-2305",
|
|
||||||
"nixpkgs-unstable": "nixpkgs-unstable",
|
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||||
"old-ghc-nix": "old-ghc-nix",
|
"old-ghc-nix": "old-ghc-nix",
|
||||||
"stackage": "stackage"
|
"stackage": "stackage",
|
||||||
|
"tullia": "tullia"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1700119633,
|
"lastModified": 1677975916,
|
||||||
"narHash": "sha256-nZY2eIo8TkRbXgJXEWMm9zor330GuUtcNzvUN9tN64U=",
|
"narHash": "sha256-dbe8lEEPyfzjdRwpePClv7J9p9lQg7BwbBqAMCw4RLw=",
|
||||||
"owner": "input-output-hk",
|
"owner": "input-output-hk",
|
||||||
"repo": "haskell.nix",
|
"repo": "haskell.nix",
|
||||||
"rev": "1fe47a3d52e1ecd6247c8ab83811a21de2e2f074",
|
"rev": "ab5efd87ce3fd8ade38a01d97693d29a4f1ae7e4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -254,91 +345,6 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hls-1.10": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1680000865,
|
|
||||||
"narHash": "sha256-rc7iiUAcrHxwRM/s0ErEsSPxOR3u8t7DvFeWlMycWgo=",
|
|
||||||
"owner": "haskell",
|
|
||||||
"repo": "haskell-language-server",
|
|
||||||
"rev": "b08691db779f7a35ff322b71e72a12f6e3376fd9",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "haskell",
|
|
||||||
"ref": "1.10.0.0",
|
|
||||||
"repo": "haskell-language-server",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hls-2.0": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1687698105,
|
|
||||||
"narHash": "sha256-OHXlgRzs/kuJH8q7Sxh507H+0Rb8b7VOiPAjcY9sM1k=",
|
|
||||||
"owner": "haskell",
|
|
||||||
"repo": "haskell-language-server",
|
|
||||||
"rev": "783905f211ac63edf982dd1889c671653327e441",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "haskell",
|
|
||||||
"ref": "2.0.0.1",
|
|
||||||
"repo": "haskell-language-server",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hls-2.2": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1693064058,
|
|
||||||
"narHash": "sha256-8DGIyz5GjuCFmohY6Fa79hHA/p1iIqubfJUTGQElbNk=",
|
|
||||||
"owner": "haskell",
|
|
||||||
"repo": "haskell-language-server",
|
|
||||||
"rev": "b30f4b6cf5822f3112c35d14a0cba51f3fe23b85",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "haskell",
|
|
||||||
"ref": "2.2.0.0",
|
|
||||||
"repo": "haskell-language-server",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hls-2.3": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1695910642,
|
|
||||||
"narHash": "sha256-tR58doOs3DncFehHwCLczJgntyG/zlsSd7DgDgMPOkI=",
|
|
||||||
"owner": "haskell",
|
|
||||||
"repo": "haskell-language-server",
|
|
||||||
"rev": "458ccdb55c9ea22cd5d13ec3051aaefb295321be",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "haskell",
|
|
||||||
"ref": "2.3.0.0",
|
|
||||||
"repo": "haskell-language-server",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hls-2.4": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1696939266,
|
|
||||||
"narHash": "sha256-VOMf5+kyOeOmfXTHlv4LNFJuDGa7G3pDnOxtzYR40IU=",
|
|
||||||
"owner": "haskell",
|
|
||||||
"repo": "haskell-language-server",
|
|
||||||
"rev": "362fdd1293efb4b82410b676ab1273479f6d17ee",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "haskell",
|
|
||||||
"ref": "2.4.0.0",
|
|
||||||
"repo": "haskell-language-server",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hpc-coveralls": {
|
"hpc-coveralls": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
@@ -378,14 +384,37 @@
|
|||||||
"type": "indirect"
|
"type": "indirect"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"incl": {
|
||||||
|
"inputs": {
|
||||||
|
"nixlib": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1669263024,
|
||||||
|
"narHash": "sha256-E/+23NKtxAqYG/0ydYgxlgarKnxmDbg6rCMWnOBqn9Q=",
|
||||||
|
"owner": "divnix",
|
||||||
|
"repo": "incl",
|
||||||
|
"rev": "ce7bebaee048e4cd7ebdb4cee7885e00c4e2abca",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "divnix",
|
||||||
|
"repo": "incl",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"iserv-proxy": {
|
"iserv-proxy": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1691634696,
|
"lastModified": 1670983692,
|
||||||
"narHash": "sha256-MZH2NznKC/gbgBu8NgIibtSUZeJ00HTLJ0PlWKCBHb0=",
|
"narHash": "sha256-avLo34JnI9HNyOuauK5R69usJm+GfW3MlyGlYxZhTgY=",
|
||||||
"ref": "hkm/remote-iserv",
|
"ref": "hkm/remote-iserv",
|
||||||
"rev": "43a979272d9addc29fbffc2e8542c5d96e993d73",
|
"rev": "50d0abb3317ac439a4e7495b185a64af9b7b9300",
|
||||||
"revCount": 14,
|
"revCount": 10,
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitlab.haskell.org/hamishmack/iserv-proxy.git"
|
"url": "https://gitlab.haskell.org/hamishmack/iserv-proxy.git"
|
||||||
},
|
},
|
||||||
@@ -411,22 +440,32 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mac2ios": {
|
"n2c": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-parts": "flake-parts",
|
"flake-utils": [
|
||||||
"nixpkgs": "nixpkgs_2"
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1699767871,
|
"lastModified": 1665039323,
|
||||||
"narHash": "sha256-kxeCUfwC/Vgh2FvVMlBUq0eVx1JvfHyN+5MPKUik9mE=",
|
"narHash": "sha256-SAh3ZjFGsaCI8FRzXQyp56qcGdAqgKEfJWPCQ0Sr7tQ=",
|
||||||
"owner": "zw3rk",
|
"owner": "nlewo",
|
||||||
"repo": "mobile-core-tools",
|
"repo": "nix2container",
|
||||||
"rev": "4dcb77d5ea896d749381806dfab5358851b08951",
|
"rev": "b008fe329ffb59b67bf9e7b08ede6ee792f2741a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "zw3rk",
|
"owner": "nlewo",
|
||||||
"repo": "mobile-core-tools",
|
"repo": "nix2container",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -451,6 +490,95 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nix-nomad": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat_2",
|
||||||
|
"flake-utils": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"nix2container",
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"gomod2nix": "gomod2nix",
|
||||||
|
"nixpkgs": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"nixpkgs-lib": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1658277770,
|
||||||
|
"narHash": "sha256-T/PgG3wUn8Z2rnzfxf2VqlR1CBjInPE0l1yVzXxPnt0=",
|
||||||
|
"owner": "tristanpemble",
|
||||||
|
"repo": "nix-nomad",
|
||||||
|
"rev": "054adcbdd0a836ae1c20951b67ed549131fd2d70",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "tristanpemble",
|
||||||
|
"repo": "nix-nomad",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nix2container": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils_3",
|
||||||
|
"nixpkgs": "nixpkgs_3"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1658567952,
|
||||||
|
"narHash": "sha256-XZ4ETYAMU7XcpEeAFP3NOl9yDXNuZAen/aIJ84G+VgA=",
|
||||||
|
"owner": "nlewo",
|
||||||
|
"repo": "nix2container",
|
||||||
|
"rev": "60bb43d405991c1378baf15a40b5811a53e32ffa",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nlewo",
|
||||||
|
"repo": "nix2container",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixago": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixago-exts": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"blank"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1661824785,
|
||||||
|
"narHash": "sha256-/PnwdWoO/JugJZHtDUioQp3uRiWeXHUdgvoyNbXesz8=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixago",
|
||||||
|
"rev": "8c1f9e5f1578d4b2ea989f618588d62a335083c3",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixago",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1657693803,
|
"lastModified": 1657693803,
|
||||||
@@ -517,11 +645,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-2205": {
|
"nixpkgs-2205": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1685573264,
|
"lastModified": 1672580127,
|
||||||
"narHash": "sha256-Zffu01pONhs/pqH07cjlF10NnMDLok8ix5Uk4rhOnZQ=",
|
"narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "380be19fbd2d9079f677978361792cb25e8a3635",
|
"rev": "0874168639713f547c05947c76124f78441ea46c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -533,11 +661,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-2211": {
|
"nixpkgs-2211": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1688392541,
|
"lastModified": 1675730325,
|
||||||
"narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=",
|
"narHash": "sha256-uNvD7fzO5hNlltNQUAFBPlcEjNG5Gkbhl/ROiX+GZU4=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b",
|
"rev": "b7ce17b1ebf600a72178f6302c77b6382d09323f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -547,40 +675,6 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-2305": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1695416179,
|
|
||||||
"narHash": "sha256-610o1+pwbSu+QuF3GE0NU5xQdTHM3t9wyYhB9l94Cd8=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "715d72e967ec1dd5ecc71290ee072bcaf5181ed6",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixpkgs-23.05-darwin",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs-lib": {
|
|
||||||
"locked": {
|
|
||||||
"dir": "lib",
|
|
||||||
"lastModified": 1696019113,
|
|
||||||
"narHash": "sha256-X3+DKYWJm93DRSdC5M6K5hLqzSya9BjibtBsuARoPco=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "f5892ddac112a1e9b3612c39af1b72987ee5783a",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"dir": "lib",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs-regression": {
|
"nixpkgs-regression": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1643052045,
|
"lastModified": 1643052045,
|
||||||
@@ -599,11 +693,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-unstable": {
|
"nixpkgs-unstable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1695318763,
|
"lastModified": 1675758091,
|
||||||
"narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=",
|
"narHash": "sha256-7gFSQbSVAFUHtGCNHPF7mPc5CcqDk9M2+inlVPZSneg=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e12483116b3b51a185a33a272bf351e357ba9a99",
|
"rev": "747927516efcb5e31ba03b7ff32f61f6d47e7d87",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -615,20 +709,82 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1698434055,
|
"lastModified": 1653581809,
|
||||||
"narHash": "sha256-Phxi5mUKSoL7A0IYUiYtkI9e8NcGaaV5PJEaJApU1Ko=",
|
"narHash": "sha256-Uvka0V5MTGbeOfWte25+tfRL3moECDh1VwokWSZUdoY=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "1a3c95e3b23b3cdb26750621c08cc2f1560cb883",
|
"rev": "83658b28fe638a170a19b8933aa008b30640fbd1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-23.05",
|
"ref": "nixos-unstable",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nixpkgs_3": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1654807842,
|
||||||
|
"narHash": "sha256-ADymZpr6LuTEBXcy6RtFHcUZdjKTBRTMYwu19WOx17E=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "fc909087cc3386955f21b4665731dbdaceefb1d8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_4": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1665087388,
|
||||||
|
"narHash": "sha256-FZFPuW9NWHJteATOf79rZfwfRn5fE0wi9kRzvGfDHPA=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "95fda953f6db2e9496d2682c4fc7b82f959878f7",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_5": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1676726892,
|
||||||
|
"narHash": "sha256-M7OYVR6dKmzmlebIjybFf3l18S2uur8lMyWWnHQooLY=",
|
||||||
|
"owner": "angerman",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "729469087592bdea58b360de59dadf6d58714c42",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "angerman",
|
||||||
|
"ref": "release-22.11",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nosys": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1667881534,
|
||||||
|
"narHash": "sha256-FhwJ15uPLRsvaxtt/bNuqE/ykMpNAPF0upozFKhTtXM=",
|
||||||
|
"owner": "divnix",
|
||||||
|
"repo": "nosys",
|
||||||
|
"rev": "2d0d5207f6a230e9d0f660903f8db9807b54814f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "divnix",
|
||||||
|
"repo": "nosys",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"old-ghc-nix": {
|
"old-ghc-nix": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
@@ -651,21 +807,17 @@
|
|||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"hackage": "hackage",
|
"hackage": "hackage",
|
||||||
"haskellNix": "haskellNix",
|
"haskellNix": "haskellNix",
|
||||||
"mac2ios": "mac2ios",
|
"nixpkgs": "nixpkgs_5"
|
||||||
"nixpkgs": [
|
|
||||||
"haskellNix",
|
|
||||||
"nixpkgs-2305"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stackage": {
|
"stackage": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1699834215,
|
"lastModified": 1677888571,
|
||||||
"narHash": "sha256-g/JKy0BCvJaxPuYDl3QVc4OY8cFEomgG+hW/eEV470M=",
|
"narHash": "sha256-YkhRNOaN6QVagZo1cfykYV8KqkI8/q6r2F5+jypOma4=",
|
||||||
"owner": "input-output-hk",
|
"owner": "input-output-hk",
|
||||||
"repo": "stackage.nix",
|
"repo": "stackage.nix",
|
||||||
"rev": "47aacd04abcce6bad57f43cbbbd133538380248e",
|
"rev": "cb50e6fabdfb2d7e655059039012ad0623f06a27",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -673,6 +825,113 @@
|
|||||||
"repo": "stackage.nix",
|
"repo": "stackage.nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"std": {
|
||||||
|
"inputs": {
|
||||||
|
"arion": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"blank"
|
||||||
|
],
|
||||||
|
"blank": "blank",
|
||||||
|
"devshell": "devshell",
|
||||||
|
"dmerge": "dmerge",
|
||||||
|
"flake-utils": "flake-utils_4",
|
||||||
|
"incl": "incl",
|
||||||
|
"makes": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"blank"
|
||||||
|
],
|
||||||
|
"microvm": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"blank"
|
||||||
|
],
|
||||||
|
"n2c": "n2c",
|
||||||
|
"nixago": "nixago",
|
||||||
|
"nixpkgs": "nixpkgs_4",
|
||||||
|
"nosys": "nosys",
|
||||||
|
"yants": "yants"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1674526466,
|
||||||
|
"narHash": "sha256-tMTaS0bqLx6VJ+K+ZT6xqsXNpzvSXJTmogkraBGzymg=",
|
||||||
|
"owner": "divnix",
|
||||||
|
"repo": "std",
|
||||||
|
"rev": "516387e3d8d059b50e742a2ff1909ed3c8f82826",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "divnix",
|
||||||
|
"repo": "std",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tullia": {
|
||||||
|
"inputs": {
|
||||||
|
"nix-nomad": "nix-nomad",
|
||||||
|
"nix2container": "nix2container",
|
||||||
|
"nixpkgs": [
|
||||||
|
"haskellNix",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"std": "std"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1675695930,
|
||||||
|
"narHash": "sha256-B7rEZ/DBUMlK1AcJ9ajnAPPxqXY6zW2SBX+51bZV0Ac=",
|
||||||
|
"owner": "input-output-hk",
|
||||||
|
"repo": "tullia",
|
||||||
|
"rev": "621365f2c725608f381b3ad5b57afef389fd4c31",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "input-output-hk",
|
||||||
|
"repo": "tullia",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"utils": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1653893745,
|
||||||
|
"narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"yants": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"haskellNix",
|
||||||
|
"tullia",
|
||||||
|
"std",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1667096281,
|
||||||
|
"narHash": "sha256-wRRec6ze0gJHmGn6m57/zhz/Kdvp9HS4Nl5fkQ+uIuA=",
|
||||||
|
"owner": "divnix",
|
||||||
|
"repo": "yants",
|
||||||
|
"rev": "d18f356ec25cb94dc9c275870c3a7927a10f8c3c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "divnix",
|
||||||
|
"repo": "yants",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
description = "nix flake for simplex-chat";
|
description = "nix flake for simplex-chat";
|
||||||
|
inputs.nixpkgs.url = "github:angerman/nixpkgs/release-22.11";
|
||||||
inputs.haskellNix.url = "github:input-output-hk/haskell.nix/armv7a";
|
inputs.haskellNix.url = "github:input-output-hk/haskell.nix/armv7a";
|
||||||
inputs.nixpkgs.follows = "haskellNix/nixpkgs-2305";
|
inputs.haskellNix.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
inputs.mac2ios.url = "github:zw3rk/mobile-core-tools";
|
|
||||||
inputs.hackage = {
|
inputs.hackage = {
|
||||||
url = "github:input-output-hk/hackage.nix";
|
url = "github:input-output-hk/hackage.nix";
|
||||||
flake = false;
|
flake = false;
|
||||||
};
|
};
|
||||||
inputs.haskellNix.inputs.hackage.follows = "hackage";
|
inputs.haskellNix.inputs.hackage.follows = "hackage";
|
||||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||||
outputs = { self, haskellNix, nixpkgs, flake-utils, mac2ios, ... }:
|
outputs = { self, haskellNix, nixpkgs, flake-utils, ... }:
|
||||||
let systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; in
|
let systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; in
|
||||||
flake-utils.lib.eachSystem systems (system:
|
flake-utils.lib.eachSystem systems (system:
|
||||||
# this android26 overlay makes the pkgsCross.{aarch64-android,armv7a-android-prebuilt} to set stdVer to 26 (Android 8).
|
# this android26 overlay makes the pkgsCross.{aarch64-android,armv7a-android-prebuilt} to set stdVer to 26 (Android 8).
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
# `appendOverlays` with a singleton is identical to `extend`.
|
# `appendOverlays` with a singleton is identical to `extend`.
|
||||||
let pkgs = haskellNix.legacyPackages.${system}.appendOverlays [android26]; in
|
let pkgs = haskellNix.legacyPackages.${system}.appendOverlays [android26]; in
|
||||||
let drv' = { extra-modules, pkgs', ... }: pkgs'.haskell-nix.project {
|
let drv' = { extra-modules, pkgs', ... }: pkgs'.haskell-nix.project {
|
||||||
compiler-nix-name = "ghc963";
|
compiler-nix-name = "ghc8107";
|
||||||
index-state = "2023-10-20T00:00:00Z";
|
index-state = "2023-10-06T00:00:00Z";
|
||||||
# We need this, to specify we want the cabal project.
|
# We need this, to specify we want the cabal project.
|
||||||
# If the stack.yaml was dropped, this would not be necessary.
|
# If the stack.yaml was dropped, this would not be necessary.
|
||||||
projectFileName = "cabal.project";
|
projectFileName = "cabal.project";
|
||||||
@@ -40,12 +40,9 @@
|
|||||||
src = ./.;
|
src = ./.;
|
||||||
};
|
};
|
||||||
sha256map = import ./scripts/nix/sha256map.nix;
|
sha256map = import ./scripts/nix/sha256map.nix;
|
||||||
modules = [
|
modules = [{
|
||||||
({ pkgs, lib, ...}: lib.mkIf (!pkgs.stdenv.hostPlatform.isWindows) {
|
|
||||||
# This patch adds `dl` as an extra-library to direct-sqlciper, which is needed
|
|
||||||
# on pretty much all unix platforms, but then blows up on windows m(
|
|
||||||
packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ];
|
packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ];
|
||||||
})
|
}
|
||||||
({ pkgs,lib, ... }: lib.mkIf (pkgs.stdenv.hostPlatform.isAndroid) {
|
({ pkgs,lib, ... }: lib.mkIf (pkgs.stdenv.hostPlatform.isAndroid) {
|
||||||
packages.simplex-chat.components.library.ghcOptions = [ "-pie" ];
|
packages.simplex-chat.components.library.ghcOptions = [ "-pie" ];
|
||||||
})] ++ extra-modules;
|
})] ++ extra-modules;
|
||||||
@@ -77,11 +74,6 @@
|
|||||||
find ${pkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \;
|
find ${pkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \;
|
||||||
# There is no static libc
|
# There is no static libc
|
||||||
${pkgs.tree}/bin/tree $out/_pkg
|
${pkgs.tree}/bin/tree $out/_pkg
|
||||||
for pkg in $out/_pkg/*.a; do
|
|
||||||
chmod +w $pkg
|
|
||||||
${mac2ios.packages.${system}.mac2ios}/bin/mac2ios $pkg
|
|
||||||
chmod -w $pkg
|
|
||||||
done
|
|
||||||
(cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/${bundleName}.zip *)
|
(cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/${bundleName}.zip *)
|
||||||
rm -fR $out/_pkg
|
rm -fR $out/_pkg
|
||||||
mkdir -p $out/nix-support
|
mkdir -p $out/nix-support
|
||||||
@@ -127,149 +119,13 @@
|
|||||||
hardeningDisable = [ "fortify" ];
|
hardeningDisable = [ "fortify" ];
|
||||||
}
|
}
|
||||||
);in {
|
);in {
|
||||||
# STATIC x86_64-linux
|
|
||||||
"${pkgs.pkgsCross.musl64.hostPlatform.system}-static:exe:simplex-chat" = (drv pkgs.pkgsCross.musl64).simplex-chat.components.exes.simplex-chat;
|
"${pkgs.pkgsCross.musl64.hostPlatform.system}-static:exe:simplex-chat" = (drv pkgs.pkgsCross.musl64).simplex-chat.components.exes.simplex-chat;
|
||||||
# STATIC i686-linux
|
"${pkgs.pkgsCross.musl32.hostPlatform.system}-static:exe:simplex-chat" = (drv pkgs.pkgsCross.musl32).simplex-chat.components.exes.simplex-chat;
|
||||||
"${pkgs.pkgsCross.musl32.hostPlatform.system}-static:exe:simplex-chat" = (drv' {
|
|
||||||
pkgs' = pkgs.pkgsCross.musl32;
|
|
||||||
extra-modules = [{
|
|
||||||
# 32 bit patches
|
|
||||||
packages.basement.patches = [
|
|
||||||
./scripts/nix/basement-pr-573.patch
|
|
||||||
];
|
|
||||||
packages.memory.patches = [
|
|
||||||
./scripts/nix/memory-pr-99.patch
|
|
||||||
];
|
|
||||||
}];
|
|
||||||
}).simplex-chat.components.exes.simplex-chat;
|
|
||||||
# WINDOWS x86_64-mingwW64
|
|
||||||
"${pkgs.pkgsCross.mingwW64.hostPlatform.system}:exe:simplex-chat" = (drv' {
|
|
||||||
pkgs' = pkgs.pkgsCross.mingwW64;
|
|
||||||
extra-modules = [{
|
|
||||||
packages.direct-sqlcipher.flags.openssl = true;
|
|
||||||
packages.bitvec.flags.simd = false;
|
|
||||||
packages.direct-sqlcipher.patches = [
|
|
||||||
./scripts/nix/direct-sqlcipher-2.3.27-win.patch
|
|
||||||
];
|
|
||||||
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
|
|
||||||
(pkgs.pkgsCross.mingwW64.openssl) #.override) # { static = true; enableKTLS = false; })
|
|
||||||
];
|
|
||||||
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
|
|
||||||
(pkgs.pkgsCross.mingwW64.openssl) #.override) # { static = true; enableKTLS = false; })
|
|
||||||
];
|
|
||||||
packages.unix-time.postPatch = ''
|
|
||||||
sed -i 's/mingwex//g' unix-time.cabal
|
|
||||||
'';
|
|
||||||
}];
|
|
||||||
}).simplex-chat.components.exes.simplex-chat.override {
|
|
||||||
postInstall = ''
|
|
||||||
set -x
|
|
||||||
${pkgs.tree}/bin/tree $out
|
|
||||||
mkdir -p $out/_pkg
|
|
||||||
cp $out/bin/* $out/_pkg
|
|
||||||
${pkgs.tree}/bin/tree $out/_pkg
|
|
||||||
(cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/${pkgs.pkgsCross.mingwW64.hostPlatform.system}-simplex-chat.zip *)
|
|
||||||
rm -fR $out/_pkg
|
|
||||||
mkdir -p $out/nix-support
|
|
||||||
echo "file binary-dist \"$(echo $out/*.zip)\"" \
|
|
||||||
> $out/nix-support/hydra-build-products
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
"${pkgs.pkgsCross.mingwW64.hostPlatform.system}:lib:simplex-chat" = (drv' rec {
|
|
||||||
pkgs' = pkgs.pkgsCross.mingwW64;
|
|
||||||
extra-modules = [{
|
|
||||||
packages.direct-sqlcipher.flags.openssl = true;
|
|
||||||
# simd will try to read __cpu_model, which we don't expose
|
|
||||||
# from the rts (yet!).
|
|
||||||
packages.bitvec.flags.simd = false;
|
|
||||||
packages.direct-sqlcipher.patches = [
|
|
||||||
./scripts/nix/direct-sqlcipher-2.3.27-win.patch
|
|
||||||
];
|
|
||||||
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
|
|
||||||
pkgs.pkgsCross.mingwW64.openssl
|
|
||||||
];
|
|
||||||
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
|
|
||||||
pkgs.pkgsCross.mingwW64.openssl
|
|
||||||
];
|
|
||||||
packages.unix-time.postPatch = ''
|
|
||||||
sed -i 's/mingwex//g' unix-time.cabal
|
|
||||||
'';
|
|
||||||
}];
|
|
||||||
}).simplex-chat.components.library
|
|
||||||
.override (p: {
|
|
||||||
# enableShared = false;
|
|
||||||
setupBuildFlags = p.component.setupBuildFlags ++ map (x: "--ghc-option=${x}") [
|
|
||||||
"-shared"
|
|
||||||
"-threaded"
|
|
||||||
"-o" "libsimplex.dll"
|
|
||||||
# "-optl-lHSrts_thr"
|
|
||||||
"-optl-lffi"
|
|
||||||
# "-optl-static-libgcc"
|
|
||||||
# We can't do -optl-static-libstdc++ with gcc. g++ might
|
|
||||||
# but then we are chaning the compiler altogether.
|
|
||||||
"${./libsimplex.dll.def}"
|
|
||||||
];
|
|
||||||
postInstall = ''
|
|
||||||
set -x
|
|
||||||
function deps() {
|
|
||||||
${pkgs.binutils}/bin/strings "$1" | grep '.\.dll'|grep -v -E 'Winsock|ADVAPI32|dbghelp|KERNEL32|msvcrt|ntdll|ole32|RPCRT4|SHELL32|USER32|WINMM|WS2_32|kernel32|GDI32'|grep -v "$1"
|
|
||||||
}
|
|
||||||
${pkgs.tree}/bin/tree $out
|
|
||||||
mkdir -p $out/_pkg
|
|
||||||
cp libsimplex.dll $out/_pkg
|
|
||||||
cp libsimplex.dll.a $out/_pkg
|
|
||||||
mkdir $out/libs
|
|
||||||
find ${pkgs.lib.getBin pkgs.pkgsCross.mingwW64.openssl} -name "*.dll" -exec cp {} $out/libs \;
|
|
||||||
find ${pkgs.lib.getBin pkgs.pkgsCross.mingwW64.libffi} -name "*.dll" -exec cp {} $out/libs \;
|
|
||||||
find ${pkgs.lib.getBin pkgs.pkgsCross.mingwW64.gmp} -name "*.dll" -exec cp {} $out/libs \;
|
|
||||||
find ${pkgs.lib.getBin pkgs.pkgsCross.mingwW64.stdenv.cc.cc} -name "*.dll" -exec cp {} $out/libs \;
|
|
||||||
find ${pkgs.lib.getBin pkgs.pkgsCross.mingwW64.windows.mcfgthreads} -name "*.dll" -exec cp {} $out/libs \;
|
|
||||||
|
|
||||||
pushd $out/_pkg
|
|
||||||
function copyDeps() {
|
|
||||||
for dep in $(deps "$1"); do
|
|
||||||
if [ ! -f "$dep" ]; then
|
|
||||||
if [ ! -f ../libs/"$dep" ]; then
|
|
||||||
echo "WARN: $1 -> $dep not found!"
|
|
||||||
else
|
|
||||||
cp ../libs/"$dep" .
|
|
||||||
copyDeps "$dep"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
copyDeps libsimplex.dll
|
|
||||||
popd
|
|
||||||
${pkgs.tree}/bin/tree $out/_pkg
|
|
||||||
(cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-${pkgs.pkgsCross.mingwW64.hostPlatform.system}-libsimplex.zip *)
|
|
||||||
rm -fR $out/_pkg
|
|
||||||
mkdir -p $out/nix-support
|
|
||||||
echo "file binary-dist \"$(echo $out/*.zip)\"" \
|
|
||||||
> $out/nix-support/hydra-build-products
|
|
||||||
'';
|
|
||||||
});
|
|
||||||
# "${pkgs.pkgsCross.muslpi.hostPlatform.system}-static:exe:simplex-chat" = (drv pkgs.pkgsCross.muslpi).simplex-chat.components.exes.simplex-chat;
|
# "${pkgs.pkgsCross.muslpi.hostPlatform.system}-static:exe:simplex-chat" = (drv pkgs.pkgsCross.muslpi).simplex-chat.components.exes.simplex-chat;
|
||||||
|
|
||||||
# STATIC aarch64-linux
|
|
||||||
"${pkgs.pkgsCross.aarch64-multiplatform-musl.hostPlatform.system}-static:exe:simplex-chat" = (drv pkgs.pkgsCross.aarch64-multiplatform-musl).simplex-chat.components.exes.simplex-chat;
|
"${pkgs.pkgsCross.aarch64-multiplatform-musl.hostPlatform.system}-static:exe:simplex-chat" = (drv pkgs.pkgsCross.aarch64-multiplatform-musl).simplex-chat.components.exes.simplex-chat;
|
||||||
"armv7a-android:lib:support" = (drv android32Pkgs).android-support.components.library.override (p: {
|
"armv7a-android:lib:support" = (drv android32Pkgs).android-support.components.library.override {
|
||||||
smallAddressSpace = true;
|
smallAddressSpace = true; enableShared = false;
|
||||||
# we won't want -dyamic (see aarch64-android:lib:simplex-chat)
|
setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsupport.so" ];
|
||||||
enableShared = false;
|
|
||||||
# we also do not want to have any dependencies listed (especially no rts!)
|
|
||||||
enableStatic = false;
|
|
||||||
|
|
||||||
# This used to work with 8.10.7...
|
|
||||||
# setupBuildFlags = p.component.setupBuildFlags ++ map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsupport.so" ];
|
|
||||||
# ... but now with 9.6+
|
|
||||||
# we have to do the -shared thing by hand.
|
|
||||||
postBuild = ''
|
|
||||||
armv7a-unknown-linux-androideabi-ghc -shared -o libsupport.so \
|
|
||||||
-optl-Wl,-u,setLineBuffering \
|
|
||||||
-optl-Wl,-u,pipe_std_to_socket \
|
|
||||||
dist/build/*.a
|
|
||||||
'';
|
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
|
|
||||||
mkdir -p $out/_pkg
|
mkdir -p $out/_pkg
|
||||||
@@ -282,29 +138,14 @@
|
|||||||
echo "file binary-dist \"$(echo $out/*.zip)\"" \
|
echo "file binary-dist \"$(echo $out/*.zip)\"" \
|
||||||
> $out/nix-support/hydra-build-products
|
> $out/nix-support/hydra-build-products
|
||||||
'';
|
'';
|
||||||
});
|
};
|
||||||
# The android-support package is at
|
"aarch64-android:lib:support" = (drv androidPkgs).android-support.components.library.override {
|
||||||
# https://github.com/simplex-chat/android-support
|
smallAddressSpace = true; enableShared = false;
|
||||||
"aarch64-android:lib:support" = (drv androidPkgs).android-support.components.library.override (p: {
|
setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsupport.so" ];
|
||||||
smallAddressSpace = true;
|
|
||||||
# no -dynamic
|
|
||||||
enableShared = false;
|
|
||||||
# but also no -staticlib
|
|
||||||
enableStatic = false;
|
|
||||||
|
|
||||||
# we have to do the -shared thing by hand.
|
|
||||||
postBuild = ''
|
|
||||||
aarch64-unknown-linux-android-ghc -shared -o libsupport.so \
|
|
||||||
-optl-Wl,-u,setLineBuffering \
|
|
||||||
-optl-Wl,-u,pipe_std_to_socket \
|
|
||||||
dist/build/*.a
|
|
||||||
'';
|
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
|
|
||||||
mkdir -p $out/_pkg
|
mkdir -p $out/_pkg
|
||||||
cp libsupport.so $out/_pkg
|
cp libsupport.so $out/_pkg
|
||||||
ls -lah $out/_pkg/*
|
|
||||||
${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsupport.so
|
${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsupport.so
|
||||||
(cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-aarch64-android-libsupport.zip *)
|
(cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-aarch64-android-libsupport.zip *)
|
||||||
rm -fR $out/_pkg
|
rm -fR $out/_pkg
|
||||||
@@ -313,11 +154,10 @@
|
|||||||
echo "file binary-dist \"$(echo $out/*.zip)\"" \
|
echo "file binary-dist \"$(echo $out/*.zip)\"" \
|
||||||
> $out/nix-support/hydra-build-products
|
> $out/nix-support/hydra-build-products
|
||||||
'';
|
'';
|
||||||
});
|
};
|
||||||
"armv7a-android:lib:simplex-chat" = (drv' {
|
"armv7a-android:lib:simplex-chat" = (drv' {
|
||||||
pkgs' = android32Pkgs;
|
pkgs' = android32Pkgs;
|
||||||
extra-modules = [{
|
extra-modules = [{
|
||||||
packages.text.flags.simdutf = false;
|
|
||||||
packages.direct-sqlcipher.flags.openssl = true;
|
packages.direct-sqlcipher.flags.openssl = true;
|
||||||
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
|
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
|
||||||
(android32Pkgs.openssl.override { static = true; enableKTLS = false; })
|
(android32Pkgs.openssl.override { static = true; enableKTLS = false; })
|
||||||
@@ -328,55 +168,13 @@
|
|||||||
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
|
packages.simplexmq.components.library.libs = pkgs.lib.mkForce [
|
||||||
(android32Pkgs.openssl.override { static = true; enableKTLS = false; })
|
(android32Pkgs.openssl.override { static = true; enableKTLS = false; })
|
||||||
];
|
];
|
||||||
# 32 bit patches
|
|
||||||
packages.basement.patches = [
|
|
||||||
./scripts/nix/basement-pr-573.patch
|
|
||||||
];
|
|
||||||
packages.memory.patches = [
|
|
||||||
./scripts/nix/memory-pr-99.patch
|
|
||||||
];
|
|
||||||
}];
|
}];
|
||||||
}).simplex-chat.components.library.override (p: {
|
}).simplex-chat.components.library.override {
|
||||||
smallAddressSpace = true;
|
smallAddressSpace = true; enableShared = false;
|
||||||
# we want -shared, but not -dyanmic, hence `enableShared = false`.
|
|
||||||
enableShared = false;
|
|
||||||
# we _do_ want rts, and other libs. Hence `enableStatic = true`.
|
|
||||||
enableStatic = true;
|
|
||||||
# for android we build a shared library, passing these arguments is a bit tricky, as
|
# for android we build a shared library, passing these arguments is a bit tricky, as
|
||||||
# we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for
|
# we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for
|
||||||
# template haskell cross compilation. Thus we just pass them as linker options (-optl).
|
# template haskell cross compilation. Thus we just pass them as linker options (-optl).
|
||||||
setupBuildFlags = p.component.setupBuildFlags
|
setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsimplex.so" "-optl-lHSrts_thr" "-optl-lffi"];
|
||||||
# flags to tell GHC we want to produce a -shared object, and we want to also link
|
|
||||||
# - the ffi library (ffi)
|
|
||||||
++ map (x: "--ghc-option=${x}") [
|
|
||||||
"-shared" "-o" "libsimplex.so"
|
|
||||||
"-threaded"
|
|
||||||
# "-debug"
|
|
||||||
"-optl-lffi"
|
|
||||||
]
|
|
||||||
# This is fairly idiotic. LLD will strip out foreign exported
|
|
||||||
# symbols (a GHC bug? Codegen bug?). So we need to pass `-u <sym>`
|
|
||||||
# to ensure they stay in the produced library. Having them
|
|
||||||
# _undefined_ and _lazy_ (lld will tell with -y <sym> that the
|
|
||||||
# symbol is lazy), makes them _defined_. m(
|
|
||||||
++ map (sym: "--ghc-option=-optl-Wl,-u,${sym}") [
|
|
||||||
"chat_close_store"
|
|
||||||
"chat_decrypt_file"
|
|
||||||
"chat_decrypt_media"
|
|
||||||
"chat_encrypt_file"
|
|
||||||
"chat_encrypt_media"
|
|
||||||
"chat_migrate_init"
|
|
||||||
"chat_parse_markdown"
|
|
||||||
"chat_parse_server"
|
|
||||||
"chat_password_hash"
|
|
||||||
"chat_read_file"
|
|
||||||
"chat_recv_msg"
|
|
||||||
"chat_recv_msg_wait"
|
|
||||||
"chat_send_cmd"
|
|
||||||
"chat_send_remote_cmd"
|
|
||||||
"chat_valid_name"
|
|
||||||
"chat_write_file"
|
|
||||||
];
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
set -x
|
set -x
|
||||||
${pkgs.tree}/bin/tree $out
|
${pkgs.tree}/bin/tree $out
|
||||||
@@ -420,11 +218,10 @@
|
|||||||
echo "file binary-dist \"$(echo $out/*.zip)\"" \
|
echo "file binary-dist \"$(echo $out/*.zip)\"" \
|
||||||
> $out/nix-support/hydra-build-products
|
> $out/nix-support/hydra-build-products
|
||||||
'';
|
'';
|
||||||
});
|
};
|
||||||
"aarch64-android:lib:simplex-chat" = (drv' {
|
"aarch64-android:lib:simplex-chat" = (drv' {
|
||||||
pkgs' = androidPkgs;
|
pkgs' = androidPkgs;
|
||||||
extra-modules = [{
|
extra-modules = [{
|
||||||
packages.text.flags.simdutf = false;
|
|
||||||
packages.direct-sqlcipher.flags.openssl = true;
|
packages.direct-sqlcipher.flags.openssl = true;
|
||||||
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
|
packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [
|
||||||
(androidPkgs.openssl.override { static = true; })
|
(androidPkgs.openssl.override { static = true; })
|
||||||
@@ -436,49 +233,12 @@
|
|||||||
(androidPkgs.openssl.override { static = true; })
|
(androidPkgs.openssl.override { static = true; })
|
||||||
];
|
];
|
||||||
}];
|
}];
|
||||||
}).simplex-chat.components.library.override (p: {
|
}).simplex-chat.components.library.override {
|
||||||
smallAddressSpace = true;
|
smallAddressSpace = true; enableShared = false;
|
||||||
# we do not want a dynamically linked object, even though we _do_
|
|
||||||
# want to produce a _shared_ object. But `shared` implied -dyanmic
|
|
||||||
# with cabal, so we disable and pass `-shared` explicitly.
|
|
||||||
enableShared = false;
|
|
||||||
# we do want static (e.g. pass all dependencies in, so we get -staticlib)
|
|
||||||
enableStatic = true;
|
|
||||||
# for android we build a shared library, passing these arguments is a bit tricky, as
|
# for android we build a shared library, passing these arguments is a bit tricky, as
|
||||||
# we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for
|
# we want only the threaded rts (HSrts_thr) and ffi to be linked, but not fed into iserv for
|
||||||
# template haskell cross compilation. Thus we just pass them as linker options (-optl).
|
# template haskell cross compilation. Thus we just pass them as linker options (-optl).
|
||||||
setupBuildFlags = p.component.setupBuildFlags
|
setupBuildFlags = map (x: "--ghc-option=${x}") [ "-shared" "-o" "libsimplex.so" "-optl-lHSrts_thr" "-optl-lffi"];
|
||||||
# flags to tell GHC we want to produce a -shared object, and we want to also link
|
|
||||||
# - the ffi library (ffi)
|
|
||||||
++ map (x: "--ghc-option=${x}") [
|
|
||||||
"-shared" "-o" "libsimplex.so"
|
|
||||||
"-threaded"
|
|
||||||
# "-debug"
|
|
||||||
"-optl-lffi"
|
|
||||||
]
|
|
||||||
# This is fairly idiotic. LLD will strip out foreign exported
|
|
||||||
# symbols (a GHC bug? Codegen bug?). So we need to pass `-u <sym>`
|
|
||||||
# to ensure they stay in the produced library. Having them
|
|
||||||
# _undefined_ and _lazy_ (lld will tell with -y <sym> that the
|
|
||||||
# symbol is lazy), makes them _defined_. m(
|
|
||||||
++ map (sym: "--ghc-option=-optl-Wl,-u,${sym}") [
|
|
||||||
"chat_close_store"
|
|
||||||
"chat_decrypt_file"
|
|
||||||
"chat_decrypt_media"
|
|
||||||
"chat_encrypt_file"
|
|
||||||
"chat_encrypt_media"
|
|
||||||
"chat_migrate_init"
|
|
||||||
"chat_parse_markdown"
|
|
||||||
"chat_parse_server"
|
|
||||||
"chat_password_hash"
|
|
||||||
"chat_read_file"
|
|
||||||
"chat_recv_msg"
|
|
||||||
"chat_recv_msg_wait"
|
|
||||||
"chat_send_cmd"
|
|
||||||
"chat_send_remote_cmd"
|
|
||||||
"chat_valid_name"
|
|
||||||
"chat_write_file"
|
|
||||||
];
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
set -x
|
set -x
|
||||||
${pkgs.tree}/bin/tree $out
|
${pkgs.tree}/bin/tree $out
|
||||||
@@ -522,7 +282,7 @@
|
|||||||
echo "file binary-dist \"$(echo $out/*.zip)\"" \
|
echo "file binary-dist \"$(echo $out/*.zip)\"" \
|
||||||
> $out/nix-support/hydra-build-products
|
> $out/nix-support/hydra-build-products
|
||||||
'';
|
'';
|
||||||
});
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# builds for iOS and iOS simulator
|
# builds for iOS and iOS simulator
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
indentation: 2
|
||||||
|
column-limit: none
|
||||||
|
function-arrows: trailing
|
||||||
|
comma-style: trailing
|
||||||
|
import-export-style: trailing
|
||||||
|
indent-wheres: true
|
||||||
|
record-brace-space: true
|
||||||
|
newlines-between-decls: 1
|
||||||
|
haddock-style: single-line
|
||||||
|
haddock-style-module: null
|
||||||
|
let-style: inline
|
||||||
|
in-style: right-align
|
||||||
|
single-constraint-parens: never
|
||||||
|
unicode: never
|
||||||
|
respectful: true
|
||||||
|
fixities:
|
||||||
|
- infixr 9 .
|
||||||
|
- infixr 8 .:, .:., .=
|
||||||
|
- infixr 6 <>
|
||||||
|
- infixr 5 ++
|
||||||
|
- infixl 4 <$>, <$, $>, <$$>, <$?>
|
||||||
|
- infixl 4 <*>, <*, *>, <**>
|
||||||
|
- infix 4 ==, /=
|
||||||
|
- infixr 3 &&
|
||||||
|
- infixl 3 <|>
|
||||||
|
- infixr 2 ||
|
||||||
|
- infixl 1 >>, >>=
|
||||||
|
- infixr 1 =<<, >=>, <=<
|
||||||
|
- infixr 0 $, $!
|
||||||
|
reexports: []
|
||||||
+2
-2
@@ -45,7 +45,7 @@ dependencies:
|
|||||||
- socks == 0.6.*
|
- socks == 0.6.*
|
||||||
- sqlcipher-simple == 0.4.*
|
- sqlcipher-simple == 0.4.*
|
||||||
- stm == 2.5.*
|
- stm == 2.5.*
|
||||||
- template-haskell == 2.20.*
|
- template-haskell >= 2.16 && < 2.21
|
||||||
- terminal == 0.2.*
|
- terminal == 0.2.*
|
||||||
- text == 2.0.*
|
- text == 2.0.*
|
||||||
- time == 1.9.*
|
- time == 1.9.*
|
||||||
@@ -124,7 +124,7 @@ tests:
|
|||||||
- async == 2.2.*
|
- async == 2.2.*
|
||||||
- deepseq == 1.4.*
|
- deepseq == 1.4.*
|
||||||
- generic-random == 1.5.*
|
- generic-random == 1.5.*
|
||||||
- hspec == 2.11.*
|
- hspec == 2.7.*
|
||||||
- network == 3.1.*
|
- network == 3.1.*
|
||||||
- silently == 1.2.*
|
- silently == 1.2.*
|
||||||
- stm == 2.5.*
|
- stm == 2.5.*
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ export type ChatResponse =
|
|||||||
| CRGroupUpdated
|
| CRGroupUpdated
|
||||||
| CRUserContactLinkSubscribed
|
| CRUserContactLinkSubscribed
|
||||||
| CRUserContactLinkSubError
|
| CRUserContactLinkSubError
|
||||||
| CRNewContactConnection
|
|
||||||
| CRContactConnectionDeleted
|
| CRContactConnectionDeleted
|
||||||
| CRMessageError
|
| CRMessageError
|
||||||
| CRChatCmdError
|
| CRChatCmdError
|
||||||
@@ -731,12 +730,6 @@ export interface CRUserContactLinkSubError extends CR {
|
|||||||
chatError: ChatError
|
chatError: ChatError
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CRNewContactConnection extends CR {
|
|
||||||
type: "newContactConnection"
|
|
||||||
user: User
|
|
||||||
connection: PendingContactConnection
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CRContactConnectionDeleted extends CR {
|
export interface CRContactConnectionDeleted extends CR {
|
||||||
type: "contactConnectionDeleted"
|
type: "contactConnectionDeleted"
|
||||||
user: User
|
user: User
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
security create-keychain -p "" simplex.keychain
|
|
||||||
security set-keychain-settings -u simplex.keychain
|
|
||||||
security add-certificates -k simplex.keychain "Developer ID Application: SimpleX Chat Ltd (5NN7GUYB6T).cer"
|
|
||||||
security add-certificates -k simplex.keychain "Developer ID Certification Authority.cer"
|
|
||||||
# Private key with access from any app
|
|
||||||
security import "SimpleX Chat.p12" -P "" -k simplex.keychain -A
|
|
||||||
# Public key
|
|
||||||
security import "SimpleX Chat.pem" -k simplex.keychain
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
trap "rm apps/multiplatform/local.properties 2> /dev/null || true; rm local.properties 2> /dev/null || true; rm /tmp/simplex.keychain" EXIT
|
trap "rm apps/multiplatform/local.properties || true; rm local.properties || true; rm /tmp/simplex.keychain || true" EXIT
|
||||||
echo "desktop.mac.signing.identity=Developer ID Application: SimpleX Chat Ltd (5NN7GUYB6T)" >> apps/multiplatform/local.properties
|
echo "desktop.mac.signing.identity=Developer ID Application: SimpleX Chat Ltd (5NN7GUYB6T)" >> apps/multiplatform/local.properties
|
||||||
echo "desktop.mac.signing.keychain=/tmp/simplex.keychain" >> apps/multiplatform/local.properties
|
echo "desktop.mac.signing.keychain=/tmp/simplex.keychain" >> apps/multiplatform/local.properties
|
||||||
echo "desktop.mac.notarization.apple_id=$APPLE_SIMPLEX_NOTARIZATION_APPLE_ID" >> apps/multiplatform/local.properties
|
echo "desktop.mac.notarization.apple_id=$APPLE_SIMPLEX_NOTARIZATION_APPLE_ID" >> apps/multiplatform/local.properties
|
||||||
@@ -10,10 +10,6 @@ echo "desktop.mac.notarization.password=$APPLE_SIMPLEX_NOTARIZATION_PASSWORD" >>
|
|||||||
echo "desktop.mac.notarization.team_id=5NN7GUYB6T" >> apps/multiplatform/local.properties
|
echo "desktop.mac.notarization.team_id=5NN7GUYB6T" >> apps/multiplatform/local.properties
|
||||||
echo "$APPLE_SIMPLEX_SIGNING_KEYCHAIN" | base64 --decode - > /tmp/simplex.keychain
|
echo "$APPLE_SIMPLEX_SIGNING_KEYCHAIN" | base64 --decode - > /tmp/simplex.keychain
|
||||||
|
|
||||||
security unlock-keychain -p "" /tmp/simplex.keychain
|
|
||||||
# Adding keychain to the list of keychains.
|
|
||||||
# Otherwise, it can find cert but exits while signing with "error: The specified item could not be found in the keychain."
|
|
||||||
security list-keychains -s `security list-keychains | xargs` /tmp/simplex.keychain
|
|
||||||
scripts/desktop/build-lib-mac.sh
|
scripts/desktop/build-lib-mac.sh
|
||||||
cd apps/multiplatform
|
cd apps/multiplatform
|
||||||
./gradlew packageDmg
|
./gradlew packageDmg
|
||||||
@@ -8,7 +8,7 @@ function readlink() {
|
|||||||
|
|
||||||
OS=linux
|
OS=linux
|
||||||
ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`}
|
ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`}
|
||||||
GHC_VERSION=9.6.3
|
GHC_VERSION=8.10.7
|
||||||
|
|
||||||
if [ "$ARCH" == "aarch64" ]; then
|
if [ "$ARCH" == "aarch64" ]; then
|
||||||
COMPOSE_ARCH=arm64
|
COMPOSE_ARCH=arm64
|
||||||
@@ -21,7 +21,7 @@ cd $root_dir
|
|||||||
BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-*
|
BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-*
|
||||||
|
|
||||||
rm -rf $BUILD_DIR
|
rm -rf $BUILD_DIR
|
||||||
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded'
|
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN' --ghc-options="-optl-L$(ghc --print-libdir)/rts -optl-Wl,--as-needed,-lHSrts_thr-ghc$GHC_VERSION"
|
||||||
cd $BUILD_DIR/build
|
cd $BUILD_DIR/build
|
||||||
#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
|
#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
|
||||||
#patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
|
#patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ set -e
|
|||||||
OS=mac
|
OS=mac
|
||||||
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
|
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
|
||||||
COMPOSE_ARCH=$ARCH
|
COMPOSE_ARCH=$ARCH
|
||||||
GHC_VERSION=9.6.3
|
GHC_VERSION=8.10.7
|
||||||
|
|
||||||
if [ "$ARCH" == "arm64" ]; then
|
if [ "$ARCH" == "arm64" ]; then
|
||||||
ARCH=aarch64
|
ARCH=aarch64
|
||||||
else
|
else
|
||||||
COMPOSE_ARCH=x64
|
COMPOSE_ARCH=x64
|
||||||
fi
|
fi
|
||||||
|
|
||||||
LIB_EXT=dylib
|
LIB_EXT=dylib
|
||||||
LIB=libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT
|
LIB=libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT
|
||||||
GHC_LIBS_DIR=$(ghc --print-libdir)
|
GHC_LIBS_DIR=$(ghc --print-libdir)
|
||||||
@@ -20,26 +19,13 @@ GHC_LIBS_DIR=$(ghc --print-libdir)
|
|||||||
BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-*
|
BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-*
|
||||||
|
|
||||||
rm -rf $BUILD_DIR
|
rm -rf $BUILD_DIR
|
||||||
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi"
|
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/rts -optl-lHSrts_thr-ghc8.10.7 -optl-lffi"
|
||||||
|
|
||||||
cd $BUILD_DIR/build
|
cd $BUILD_DIR/build
|
||||||
mkdir deps 2> /dev/null || true
|
mkdir deps 2> /dev/null || true
|
||||||
|
|
||||||
# It's not included by default for some reason. Compiled lib tries to find system one but it's not always available
|
# It's not included by default for some reason. Compiled lib tries to find system one but it's not always available
|
||||||
#cp $GHC_LIBS_DIR/libffi.dylib ./deps
|
cp $GHC_LIBS_DIR/rts/libffi.dylib ./deps
|
||||||
(
|
|
||||||
BUILD=$PWD
|
|
||||||
cp /tmp/libffi-3.4.4/*-apple-darwin*/.libs/libffi.dylib $BUILD/deps || \
|
|
||||||
( \
|
|
||||||
cd /tmp && \
|
|
||||||
curl "https://gitlab.haskell.org/ghc/libffi-tarballs/-/raw/libffi-3.4.4/libffi-3.4.4.tar.gz?inline=false" -o libffi.tar.gz && \
|
|
||||||
tar -xzvf libffi.tar.gz && \
|
|
||||||
cd "libffi-3.4.4" && \
|
|
||||||
./configure && \
|
|
||||||
make && \
|
|
||||||
cp *-apple-darwin*/.libs/libffi.dylib $BUILD/deps \
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
DYLIBS=`otool -L $LIB | grep @rpath | tail -n +2 | cut -d' ' -f 1 | cut -d'/' -f2`
|
DYLIBS=`otool -L $LIB | grep @rpath | tail -n +2 | cut -d' ' -f 1 | cut -d'/' -f2`
|
||||||
RPATHS=`otool -l $LIB | grep "path "| cut -d' ' -f11`
|
RPATHS=`otool -l $LIB | grep "path "| cut -d' ' -f11`
|
||||||
@@ -80,8 +66,6 @@ function copy_deps() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
copy_deps $LIB
|
copy_deps $LIB
|
||||||
# Special case
|
|
||||||
cp $(ghc --print-libdir)/$ARCH-osx-ghc-$GHC_VERSION/libHSghc-boot-th-$GHC_VERSION-ghc$GHC_VERSION.dylib deps
|
|
||||||
rm deps/`basename $LIB`
|
rm deps/`basename $LIB`
|
||||||
|
|
||||||
cd -
|
cd -
|
||||||
|
|||||||
@@ -1,242 +0,0 @@
|
|||||||
From 38be2c93acb6f459d24ed6c626981c35ccf44095 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Sylvain Henry <sylvain@haskus.fr>
|
|
||||||
Date: Thu, 16 Feb 2023 15:40:45 +0100
|
|
||||||
Subject: [PATCH] Fix build on 32-bit architectures
|
|
||||||
|
|
||||||
---
|
|
||||||
Basement/Bits.hs | 4 ++++
|
|
||||||
Basement/From.hs | 24 -----------------------
|
|
||||||
Basement/Numerical/Additive.hs | 4 ++++
|
|
||||||
Basement/Numerical/Conversion.hs | 20 +++++++++++++++++++
|
|
||||||
Basement/PrimType.hs | 6 +++++-
|
|
||||||
Basement/Types/OffsetSize.hs | 22 +++++++++++++++++++--
|
|
||||||
6 files changed, 53 insertions(+), 27 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/Basement/Bits.hs b/Basement/Bits.hs
|
|
||||||
index 7eeea0f5..24520ed7 100644
|
|
||||||
--- a/Basement/Bits.hs
|
|
||||||
+++ b/Basement/Bits.hs
|
|
||||||
@@ -54,8 +54,12 @@ import GHC.Int
|
|
||||||
import Basement.Compat.Primitive
|
|
||||||
|
|
||||||
#if WORD_SIZE_IN_BITS < 64
|
|
||||||
+#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
+import GHC.Exts
|
|
||||||
+#else
|
|
||||||
import GHC.IntWord64
|
|
||||||
#endif
|
|
||||||
+#endif
|
|
||||||
|
|
||||||
-- | operation over finite bits
|
|
||||||
class FiniteBitsOps bits where
|
|
||||||
diff --git a/Basement/From.hs b/Basement/From.hs
|
|
||||||
index 7bbe141c..80014b3e 100644
|
|
||||||
--- a/Basement/From.hs
|
|
||||||
+++ b/Basement/From.hs
|
|
||||||
@@ -272,23 +272,11 @@ instance (NatWithinBound (CountOf ty) n, KnownNat n, PrimType ty)
|
|
||||||
tryFrom = BlockN.toBlockN . UArray.toBlock . BoxArray.mapToUnboxed id
|
|
||||||
|
|
||||||
instance (KnownNat n, NatWithinBound Word8 n) => From (Zn64 n) Word8 where
|
|
||||||
-#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
- from = narrow . unZn64 where narrow (W64# w) = W8# (wordToWord8# (word64ToWord# (GHC.Prim.word64ToWord# w)))
|
|
||||||
-#else
|
|
||||||
from = narrow . unZn64 where narrow (W64# w) = W8# (wordToWord8# (word64ToWord# w))
|
|
||||||
-#endif
|
|
||||||
instance (KnownNat n, NatWithinBound Word16 n) => From (Zn64 n) Word16 where
|
|
||||||
-#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
- from = narrow . unZn64 where narrow (W64# w) = W16# (wordToWord16# (word64ToWord# (GHC.Prim.word64ToWord# w)))
|
|
||||||
-#else
|
|
||||||
from = narrow . unZn64 where narrow (W64# w) = W16# (wordToWord16# (word64ToWord# w))
|
|
||||||
-#endif
|
|
||||||
instance (KnownNat n, NatWithinBound Word32 n) => From (Zn64 n) Word32 where
|
|
||||||
-#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
- from = narrow . unZn64 where narrow (W64# w) = W32# (wordToWord32# (word64ToWord# (GHC.Prim.word64ToWord# w)))
|
|
||||||
-#else
|
|
||||||
from = narrow . unZn64 where narrow (W64# w) = W32# (wordToWord32# (word64ToWord# w))
|
|
||||||
-#endif
|
|
||||||
instance From (Zn64 n) Word64 where
|
|
||||||
from = unZn64
|
|
||||||
instance From (Zn64 n) Word128 where
|
|
||||||
@@ -297,23 +285,11 @@ instance From (Zn64 n) Word256 where
|
|
||||||
from = from . unZn64
|
|
||||||
|
|
||||||
instance (KnownNat n, NatWithinBound Word8 n) => From (Zn n) Word8 where
|
|
||||||
-#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
- from = narrow . naturalToWord64 . unZn where narrow (W64# w) = W8# (wordToWord8# (word64ToWord# (GHC.Prim.word64ToWord# w)))
|
|
||||||
-#else
|
|
||||||
from = narrow . naturalToWord64 . unZn where narrow (W64# w) = W8# (wordToWord8# (word64ToWord# w))
|
|
||||||
-#endif
|
|
||||||
instance (KnownNat n, NatWithinBound Word16 n) => From (Zn n) Word16 where
|
|
||||||
-#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
- from = narrow . naturalToWord64 . unZn where narrow (W64# w) = W16# (wordToWord16# (word64ToWord# (GHC.Prim.word64ToWord# w)))
|
|
||||||
-#else
|
|
||||||
from = narrow . naturalToWord64 . unZn where narrow (W64# w) = W16# (wordToWord16# (word64ToWord# w))
|
|
||||||
-#endif
|
|
||||||
instance (KnownNat n, NatWithinBound Word32 n) => From (Zn n) Word32 where
|
|
||||||
-#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
- from = narrow . naturalToWord64 . unZn where narrow (W64# w) = W32# (wordToWord32# (word64ToWord# (GHC.Prim.word64ToWord# w)))
|
|
||||||
-#else
|
|
||||||
from = narrow . naturalToWord64 . unZn where narrow (W64# w) = W32# (wordToWord32# (word64ToWord# w))
|
|
||||||
-#endif
|
|
||||||
instance (KnownNat n, NatWithinBound Word64 n) => From (Zn n) Word64 where
|
|
||||||
from = naturalToWord64 . unZn
|
|
||||||
instance (KnownNat n, NatWithinBound Word128 n) => From (Zn n) Word128 where
|
|
||||||
diff --git a/Basement/Numerical/Additive.hs b/Basement/Numerical/Additive.hs
|
|
||||||
index d0dfb973..8ab65aa0 100644
|
|
||||||
--- a/Basement/Numerical/Additive.hs
|
|
||||||
+++ b/Basement/Numerical/Additive.hs
|
|
||||||
@@ -30,8 +30,12 @@ import qualified Basement.Types.Word128 as Word128
|
|
||||||
import qualified Basement.Types.Word256 as Word256
|
|
||||||
|
|
||||||
#if WORD_SIZE_IN_BITS < 64
|
|
||||||
+#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
+import GHC.Exts
|
|
||||||
+#else
|
|
||||||
import GHC.IntWord64
|
|
||||||
#endif
|
|
||||||
+#endif
|
|
||||||
|
|
||||||
-- | Represent class of things that can be added together,
|
|
||||||
-- contains a neutral element and is commutative.
|
|
||||||
diff --git a/Basement/Numerical/Conversion.hs b/Basement/Numerical/Conversion.hs
|
|
||||||
index db502c07..fddc8232 100644
|
|
||||||
--- a/Basement/Numerical/Conversion.hs
|
|
||||||
+++ b/Basement/Numerical/Conversion.hs
|
|
||||||
@@ -26,8 +26,12 @@ import GHC.Word
|
|
||||||
import Basement.Compat.Primitive
|
|
||||||
|
|
||||||
#if WORD_SIZE_IN_BITS < 64
|
|
||||||
+#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
+import GHC.Exts
|
|
||||||
+#else
|
|
||||||
import GHC.IntWord64
|
|
||||||
#endif
|
|
||||||
+#endif
|
|
||||||
|
|
||||||
intToInt64 :: Int -> Int64
|
|
||||||
#if WORD_SIZE_IN_BITS == 64
|
|
||||||
@@ -96,11 +100,22 @@ int64ToWord64 (I64# i) = W64# (int64ToWord64# i)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if WORD_SIZE_IN_BITS == 64
|
|
||||||
+#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
+word64ToWord# :: Word64# -> Word#
|
|
||||||
+word64ToWord# i = word64ToWord# i
|
|
||||||
+#else
|
|
||||||
word64ToWord# :: Word# -> Word#
|
|
||||||
word64ToWord# i = i
|
|
||||||
+#endif
|
|
||||||
{-# INLINE word64ToWord# #-}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
+#if WORD_SIZE_IN_BITS < 64
|
|
||||||
+word64ToWord32# :: Word64# -> Word32#
|
|
||||||
+word64ToWord32# i = wordToWord32# (word64ToWord# i)
|
|
||||||
+{-# INLINE word64ToWord32# #-}
|
|
||||||
+#endif
|
|
||||||
+
|
|
||||||
-- | 2 Word32s
|
|
||||||
data Word32x2 = Word32x2 {-# UNPACK #-} !Word32
|
|
||||||
{-# UNPACK #-} !Word32
|
|
||||||
@@ -113,9 +128,14 @@ word64ToWord32s (W64# w64) = Word32x2 (W32# (wordToWord32# (uncheckedShiftRL# (G
|
|
||||||
word64ToWord32s (W64# w64) = Word32x2 (W32# (wordToWord32# (uncheckedShiftRL# w64 32#))) (W32# (wordToWord32# w64))
|
|
||||||
#endif
|
|
||||||
#else
|
|
||||||
+#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
+word64ToWord32s :: Word64 -> Word32x2
|
|
||||||
+word64ToWord32s (W64# w64) = Word32x2 (W32# (word64ToWord32# (uncheckedShiftRL64# w64 32#))) (W32# (word64ToWord32# w64))
|
|
||||||
+#else
|
|
||||||
word64ToWord32s :: Word64 -> Word32x2
|
|
||||||
word64ToWord32s (W64# w64) = Word32x2 (W32# (word64ToWord# (uncheckedShiftRL64# w64 32#))) (W32# (word64ToWord# w64))
|
|
||||||
#endif
|
|
||||||
+#endif
|
|
||||||
|
|
||||||
wordToChar :: Word -> Char
|
|
||||||
wordToChar (W# word) = C# (chr# (word2Int# word))
|
|
||||||
diff --git a/Basement/PrimType.hs b/Basement/PrimType.hs
|
|
||||||
index f8ca2926..a888ec91 100644
|
|
||||||
--- a/Basement/PrimType.hs
|
|
||||||
+++ b/Basement/PrimType.hs
|
|
||||||
@@ -54,7 +54,11 @@ import Basement.Nat
|
|
||||||
import qualified Prelude (quot)
|
|
||||||
|
|
||||||
#if WORD_SIZE_IN_BITS < 64
|
|
||||||
-import GHC.IntWord64
|
|
||||||
+#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
+import GHC.Exts
|
|
||||||
+#else
|
|
||||||
+import GHC.IntWord64
|
|
||||||
+#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef FOUNDATION_BOUNDS_CHECK
|
|
||||||
diff --git a/Basement/Types/OffsetSize.hs b/Basement/Types/OffsetSize.hs
|
|
||||||
index cd944927..1ea80dad 100644
|
|
||||||
--- a/Basement/Types/OffsetSize.hs
|
|
||||||
+++ b/Basement/Types/OffsetSize.hs
|
|
||||||
@@ -70,8 +70,12 @@ import Data.List (foldl')
|
|
||||||
import qualified Prelude
|
|
||||||
|
|
||||||
#if WORD_SIZE_IN_BITS < 64
|
|
||||||
+#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
+import GHC.Exts
|
|
||||||
+#else
|
|
||||||
import GHC.IntWord64
|
|
||||||
#endif
|
|
||||||
+#endif
|
|
||||||
|
|
||||||
-- | File size in bytes
|
|
||||||
newtype FileSize = FileSize Word64
|
|
||||||
@@ -225,20 +229,26 @@ countOfRoundUp alignment (CountOf n) = CountOf ((n + (alignment-1)) .&. compleme
|
|
||||||
|
|
||||||
csizeOfSize :: CountOf Word8 -> CSize
|
|
||||||
#if WORD_SIZE_IN_BITS < 64
|
|
||||||
+#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
+csizeOfSize (CountOf (I# sz)) = CSize (W32# (wordToWord32# (int2Word# sz)))
|
|
||||||
+#else
|
|
||||||
csizeOfSize (CountOf (I# sz)) = CSize (W32# (int2Word# sz))
|
|
||||||
+#endif
|
|
||||||
#else
|
|
||||||
#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
csizeOfSize (CountOf (I# sz)) = CSize (W64# (wordToWord64# (int2Word# sz)))
|
|
||||||
-
|
|
||||||
#else
|
|
||||||
csizeOfSize (CountOf (I# sz)) = CSize (W64# (int2Word# sz))
|
|
||||||
-
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
csizeOfOffset :: Offset8 -> CSize
|
|
||||||
#if WORD_SIZE_IN_BITS < 64
|
|
||||||
+#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
+csizeOfOffset (Offset (I# sz)) = CSize (W32# (wordToWord32# (int2Word# sz)))
|
|
||||||
+#else
|
|
||||||
csizeOfOffset (Offset (I# sz)) = CSize (W32# (int2Word# sz))
|
|
||||||
+#endif
|
|
||||||
#else
|
|
||||||
#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
csizeOfOffset (Offset (I# sz)) = CSize (W64# (wordToWord64# (int2Word# sz)))
|
|
||||||
@@ -250,7 +260,11 @@ csizeOfOffset (Offset (I# sz)) = CSize (W64# (int2Word# sz))
|
|
||||||
sizeOfCSSize :: CSsize -> CountOf Word8
|
|
||||||
sizeOfCSSize (CSsize (-1)) = error "invalid size: CSSize is -1"
|
|
||||||
#if WORD_SIZE_IN_BITS < 64
|
|
||||||
+#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
+sizeOfCSSize (CSsize (I32# sz)) = CountOf (I# (int32ToInt# sz))
|
|
||||||
+#else
|
|
||||||
sizeOfCSSize (CSsize (I32# sz)) = CountOf (I# sz)
|
|
||||||
+#endif
|
|
||||||
#else
|
|
||||||
#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
sizeOfCSSize (CSsize (I64# sz)) = CountOf (I# (int64ToInt# sz))
|
|
||||||
@@ -261,7 +275,11 @@ sizeOfCSSize (CSsize (I64# sz)) = CountOf (I# sz)
|
|
||||||
|
|
||||||
sizeOfCSize :: CSize -> CountOf Word8
|
|
||||||
#if WORD_SIZE_IN_BITS < 64
|
|
||||||
+#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
+sizeOfCSize (CSize (W32# sz)) = CountOf (I# (word2Int# (word32ToWord# sz)))
|
|
||||||
+#else
|
|
||||||
sizeOfCSize (CSize (W32# sz)) = CountOf (I# (word2Int# sz))
|
|
||||||
+#endif
|
|
||||||
#else
|
|
||||||
#if __GLASGOW_HASKELL__ >= 904
|
|
||||||
sizeOfCSize (CSize (W64# sz)) = CountOf (I# (word2Int# (word64ToWord# sz)))
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
diff --git a/direct-sqlcipher.cabal b/direct-sqlcipher.cabal
|
|
||||||
index 728ba3e..c63745e 100644
|
|
||||||
--- a/direct-sqlcipher.cabal
|
|
||||||
+++ b/direct-sqlcipher.cabal
|
|
||||||
@@ -84,6 +84,8 @@ library
|
|
||||||
cc-options: -DSQLITE_TEMP_STORE=2
|
|
||||||
-DSQLITE_HAS_CODEC
|
|
||||||
|
|
||||||
+ extra-libraries: ws2_32
|
|
||||||
+
|
|
||||||
if !os(windows) && !os(android)
|
|
||||||
extra-libraries: pthread
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
From 2738929ce15b4c8704bbbac24a08539b5d4bf30e Mon Sep 17 00:00:00 2001
|
|
||||||
From: sternenseemann <sternenseemann@systemli.org>
|
|
||||||
Date: Mon, 14 Aug 2023 10:51:30 +0200
|
|
||||||
Subject: [PATCH] Data.Memory.Internal.CompatPrim64: fix 32 bit with GHC >= 9.4
|
|
||||||
|
|
||||||
Since 9.4, GHC.Prim exports Word64# operations like timesWord64# even on
|
|
||||||
i686 whereas GHC.IntWord64 no longer exists. Therefore, we can just use
|
|
||||||
the ready made solution.
|
|
||||||
|
|
||||||
Closes #98, as it should be the better solution.
|
|
||||||
---
|
|
||||||
Data/Memory/Internal/CompatPrim64.hs | 4 ++++
|
|
||||||
1 file changed, 4 insertions(+)
|
|
||||||
|
|
||||||
diff --git a/Data/Memory/Internal/CompatPrim64.hs b/Data/Memory/Internal/CompatPrim64.hs
|
|
||||||
index b9eef8a..a134c88 100644
|
|
||||||
--- a/Data/Memory/Internal/CompatPrim64.hs
|
|
||||||
+++ b/Data/Memory/Internal/CompatPrim64.hs
|
|
||||||
@@ -150,6 +150,7 @@ w64# :: Word# -> Word# -> Word# -> Word64#
|
|
||||||
w64# w _ _ = w
|
|
||||||
|
|
||||||
#elif WORD_SIZE_IN_BITS == 32
|
|
||||||
+#if __GLASGOW_HASKELL__ < 904
|
|
||||||
import GHC.IntWord64
|
|
||||||
import GHC.Prim (Word#)
|
|
||||||
|
|
||||||
@@ -158,6 +159,9 @@ timesWord64# a b =
|
|
||||||
let !ai = word64ToInt64# a
|
|
||||||
!bi = word64ToInt64# b
|
|
||||||
in int64ToWord64# (timesInt64# ai bi)
|
|
||||||
+#else
|
|
||||||
+import GHC.Prim
|
|
||||||
+#endif
|
|
||||||
|
|
||||||
w64# :: Word# -> Word# -> Word# -> Word64#
|
|
||||||
w64# _ hw lw =
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."757b7eec81341d8560a326deab303bb6fb6a26a3" = "0kqnxpyz8v43802fncqxdg6i2ni70yv7jg7a1nbkny1w937fwf40";
|
"https://github.com/simplex-chat/simplexmq.git"."140b4b12bdee809bcb5484101b5f1b753c744b51" = "19ardk84b25bh22aab0rjvlar966db2wc8hhin1kyh4m66dy92kx";
|
||||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||||
"https://github.com/kazu-yamamoto/http2.git"."f5525b755ff2418e6e6ecc69e877363b0d0bcaeb" = "0fyx0047gvhm99ilp212mmz37j84cwrfnpmssib5dw363fyb88b6";
|
"https://github.com/kazu-yamamoto/http2.git"."f5525b755ff2418e6e6ecc69e877363b0d0bcaeb" = "0fyx0047gvhm99ilp212mmz37j84cwrfnpmssib5dw363fyb88b6";
|
||||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||||
@@ -7,5 +7,5 @@
|
|||||||
"https://github.com/simplex-chat/aeson.git"."aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b" = "0jz7kda8gai893vyvj96fy962ncv8dcsx71fbddyy8zrvc88jfrr";
|
"https://github.com/simplex-chat/aeson.git"."aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b" = "0jz7kda8gai893vyvj96fy962ncv8dcsx71fbddyy8zrvc88jfrr";
|
||||||
"https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
|
"https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
|
||||||
"https://github.com/simplex-chat/android-support.git"."9aa09f148089d6752ce563b14c2df1895718d806" = "0pbf2pf13v2kjzi397nr13f1h3jv0imvsq8rpiyy2qyx5vd50pqn";
|
"https://github.com/simplex-chat/android-support.git"."9aa09f148089d6752ce563b14c2df1895718d806" = "0pbf2pf13v2kjzi397nr13f1h3jv0imvsq8rpiyy2qyx5vd50pqn";
|
||||||
"https://github.com/simplex-chat/network-transport.git"."0013798272a683e35ca38d2fdaf480942311fba8" = "0dnn62apgvc248df0m8ib7phrzn63wm0xs71xvlypv52j6cgwzkb";
|
"https://github.com/simplex-chat/zip.git"."bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc" = "1csqfjhvc8wb5h4kxxndmb6iw7b4ib9ff2n81hrizsmnf45a6gg0";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ BEGIN {
|
|||||||
/tag/ && isGit == true { ref=$2 }
|
/tag/ && isGit == true { ref=$2 }
|
||||||
|
|
||||||
isGit == true && loc != "" && ref != "" {
|
isGit == true && loc != "" && ref != "" {
|
||||||
cmd = "nix-prefetch-git --fetch-submodules --quiet "loc" "ref" | jq -r .sha256"
|
cmd = "nix-prefetch-git --quiet "loc" "ref" | jq -r .sha256"
|
||||||
cmd | getline sha256
|
cmd | getline sha256
|
||||||
close(cmd)
|
close(cmd)
|
||||||
print " \""loc"\".\""ref"\" = \""sha256"\";";
|
print " \""loc"\".\""ref"\" = \""sha256"\";";
|
||||||
|
|||||||
+10
-9
@@ -1,6 +1,6 @@
|
|||||||
cabal-version: 1.12
|
cabal-version: 1.12
|
||||||
|
|
||||||
-- This file has been generated from package.yaml by hpack version 0.36.0.
|
-- This file has been generated from package.yaml by hpack version 0.35.0.
|
||||||
--
|
--
|
||||||
-- see: https://github.com/sol/hpack
|
-- see: https://github.com/sol/hpack
|
||||||
|
|
||||||
@@ -124,6 +124,7 @@ library
|
|||||||
Simplex.Chat.Migrations.M20231107_indexes
|
Simplex.Chat.Migrations.M20231107_indexes
|
||||||
Simplex.Chat.Migrations.M20231113_group_forward
|
Simplex.Chat.Migrations.M20231113_group_forward
|
||||||
Simplex.Chat.Migrations.M20231114_remote_control
|
Simplex.Chat.Migrations.M20231114_remote_control
|
||||||
|
Simplex.Chat.Migrations.M20231126_remote_ctrl_address
|
||||||
Simplex.Chat.Mobile
|
Simplex.Chat.Mobile
|
||||||
Simplex.Chat.Mobile.File
|
Simplex.Chat.Mobile.File
|
||||||
Simplex.Chat.Mobile.Shared
|
Simplex.Chat.Mobile.Shared
|
||||||
@@ -196,7 +197,7 @@ library
|
|||||||
, socks ==0.6.*
|
, socks ==0.6.*
|
||||||
, sqlcipher-simple ==0.4.*
|
, sqlcipher-simple ==0.4.*
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
, template-haskell ==2.20.*
|
, template-haskell >=2.16 && <2.21
|
||||||
, terminal ==0.2.*
|
, terminal ==0.2.*
|
||||||
, text ==2.0.*
|
, text ==2.0.*
|
||||||
, time ==1.9.*
|
, time ==1.9.*
|
||||||
@@ -249,7 +250,7 @@ executable simplex-bot
|
|||||||
, socks ==0.6.*
|
, socks ==0.6.*
|
||||||
, sqlcipher-simple ==0.4.*
|
, sqlcipher-simple ==0.4.*
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
, template-haskell ==2.20.*
|
, template-haskell >=2.16 && <2.21
|
||||||
, terminal ==0.2.*
|
, terminal ==0.2.*
|
||||||
, text ==2.0.*
|
, text ==2.0.*
|
||||||
, time ==1.9.*
|
, time ==1.9.*
|
||||||
@@ -302,7 +303,7 @@ executable simplex-bot-advanced
|
|||||||
, socks ==0.6.*
|
, socks ==0.6.*
|
||||||
, sqlcipher-simple ==0.4.*
|
, sqlcipher-simple ==0.4.*
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
, template-haskell ==2.20.*
|
, template-haskell >=2.16 && <2.21
|
||||||
, terminal ==0.2.*
|
, terminal ==0.2.*
|
||||||
, text ==2.0.*
|
, text ==2.0.*
|
||||||
, time ==1.9.*
|
, time ==1.9.*
|
||||||
@@ -357,7 +358,7 @@ executable simplex-broadcast-bot
|
|||||||
, socks ==0.6.*
|
, socks ==0.6.*
|
||||||
, sqlcipher-simple ==0.4.*
|
, sqlcipher-simple ==0.4.*
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
, template-haskell ==2.20.*
|
, template-haskell >=2.16 && <2.21
|
||||||
, terminal ==0.2.*
|
, terminal ==0.2.*
|
||||||
, text ==2.0.*
|
, text ==2.0.*
|
||||||
, time ==1.9.*
|
, time ==1.9.*
|
||||||
@@ -411,7 +412,7 @@ executable simplex-chat
|
|||||||
, socks ==0.6.*
|
, socks ==0.6.*
|
||||||
, sqlcipher-simple ==0.4.*
|
, sqlcipher-simple ==0.4.*
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
, template-haskell ==2.20.*
|
, template-haskell >=2.16 && <2.21
|
||||||
, terminal ==0.2.*
|
, terminal ==0.2.*
|
||||||
, text ==2.0.*
|
, text ==2.0.*
|
||||||
, time ==1.9.*
|
, time ==1.9.*
|
||||||
@@ -469,7 +470,7 @@ executable simplex-directory-service
|
|||||||
, socks ==0.6.*
|
, socks ==0.6.*
|
||||||
, sqlcipher-simple ==0.4.*
|
, sqlcipher-simple ==0.4.*
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
, template-haskell ==2.20.*
|
, template-haskell >=2.16 && <2.21
|
||||||
, terminal ==0.2.*
|
, terminal ==0.2.*
|
||||||
, text ==2.0.*
|
, text ==2.0.*
|
||||||
, time ==1.9.*
|
, time ==1.9.*
|
||||||
@@ -536,7 +537,7 @@ test-suite simplex-chat-test
|
|||||||
, exceptions ==0.10.*
|
, exceptions ==0.10.*
|
||||||
, filepath ==1.4.*
|
, filepath ==1.4.*
|
||||||
, generic-random ==1.5.*
|
, generic-random ==1.5.*
|
||||||
, hspec ==2.11.*
|
, hspec ==2.7.*
|
||||||
, http-types ==0.12.*
|
, http-types ==0.12.*
|
||||||
, http2 >=4.2.2 && <4.3
|
, http2 >=4.2.2 && <4.3
|
||||||
, memory ==0.18.*
|
, memory ==0.18.*
|
||||||
@@ -554,7 +555,7 @@ test-suite simplex-chat-test
|
|||||||
, socks ==0.6.*
|
, socks ==0.6.*
|
||||||
, sqlcipher-simple ==0.4.*
|
, sqlcipher-simple ==0.4.*
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
, template-haskell ==2.20.*
|
, template-haskell >=2.16 && <2.21
|
||||||
, terminal ==0.2.*
|
, terminal ==0.2.*
|
||||||
, text ==2.0.*
|
, text ==2.0.*
|
||||||
, time ==1.9.*
|
, time ==1.9.*
|
||||||
|
|||||||
+136
-129
File diff suppressed because one or more lines are too long
@@ -22,7 +22,7 @@ import qualified Data.Text as T
|
|||||||
import qualified Database.SQLite3 as SQL
|
import qualified Database.SQLite3 as SQL
|
||||||
import Simplex.Chat.Controller
|
import Simplex.Chat.Controller
|
||||||
import Simplex.Messaging.Agent.Client (agentClientStore)
|
import Simplex.Messaging.Agent.Client (agentClientStore)
|
||||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), sqlString, closeSQLiteStore)
|
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), closeSQLiteStore, sqlString)
|
||||||
import Simplex.Messaging.Util
|
import Simplex.Messaging.Util
|
||||||
import System.FilePath
|
import System.FilePath
|
||||||
import UnliftIO.Directory
|
import UnliftIO.Directory
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ module Simplex.Chat.Bot where
|
|||||||
import Control.Concurrent.Async
|
import Control.Concurrent.Async
|
||||||
import Control.Concurrent.STM
|
import Control.Concurrent.STM
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
import Control.Monad.Reader
|
||||||
import qualified Data.ByteString.Char8 as B
|
import qualified Data.ByteString.Char8 as B
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Simplex.Chat.Controller
|
import Simplex.Chat.Controller
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ module Simplex.Chat.Bot.KnownContacts where
|
|||||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Text.Encoding (encodeUtf8)
|
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import Data.Text.Encoding (encodeUtf8)
|
||||||
import Options.Applicative
|
import Options.Applicative
|
||||||
import Simplex.Messaging.Parsers (parseAll)
|
import Simplex.Messaging.Parsers (parseAll)
|
||||||
import Simplex.Messaging.Util (safeDecodeUtf8)
|
import Simplex.Messaging.Util (safeDecodeUtf8)
|
||||||
|
|||||||
@@ -225,4 +225,3 @@ instance FromField CallState where
|
|||||||
fromField = fromTextField_ decodeJSON
|
fromField = fromTextField_ decodeJSON
|
||||||
|
|
||||||
$(J.deriveJSON defaultJSON ''RcvCallInvitation)
|
$(J.deriveJSON defaultJSON ''RcvCallInvitation)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{-# LANGUAGE ConstraintKinds #-}
|
|
||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
|
{-# LANGUAGE ConstraintKinds #-}
|
||||||
{-# LANGUAGE DataKinds #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
{-# LANGUAGE DeriveAnyClass #-}
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
@@ -41,6 +41,7 @@ import Data.String
|
|||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Time (NominalDiffTime, UTCTime)
|
import Data.Time (NominalDiffTime, UTCTime)
|
||||||
import Data.Version (showVersion)
|
import Data.Version (showVersion)
|
||||||
|
import Data.Word (Word16)
|
||||||
import Language.Haskell.TH (Exp, Q, runIO)
|
import Language.Haskell.TH (Exp, Q, runIO)
|
||||||
import Numeric.Natural
|
import Numeric.Natural
|
||||||
import qualified Paths_simplex_chat as SC
|
import qualified Paths_simplex_chat as SC
|
||||||
@@ -426,19 +427,19 @@ data ChatCommand
|
|||||||
| SetGroupTimedMessages GroupName (Maybe Int)
|
| SetGroupTimedMessages GroupName (Maybe Int)
|
||||||
| SetLocalDeviceName Text
|
| SetLocalDeviceName Text
|
||||||
| ListRemoteHosts
|
| ListRemoteHosts
|
||||||
| StartRemoteHost (Maybe (RemoteHostId, Bool)) -- ^ Start new or known remote host with optional multicast for known host
|
| StartRemoteHost (Maybe (RemoteHostId, Bool)) (Maybe RCCtrlAddress) (Maybe Word16) -- Start new or known remote host with optional multicast for known host
|
||||||
| SwitchRemoteHost (Maybe RemoteHostId) -- ^ Switch current remote host
|
| SwitchRemoteHost (Maybe RemoteHostId) -- Switch current remote host
|
||||||
| StopRemoteHost RHKey -- ^ Shut down a running session
|
| StopRemoteHost RHKey -- Shut down a running session
|
||||||
| DeleteRemoteHost RemoteHostId -- ^ Unregister remote host and remove its data
|
| DeleteRemoteHost RemoteHostId -- Unregister remote host and remove its data
|
||||||
| StoreRemoteFile {remoteHostId :: RemoteHostId, storeEncrypted :: Maybe Bool, localPath :: FilePath}
|
| StoreRemoteFile {remoteHostId :: RemoteHostId, storeEncrypted :: Maybe Bool, localPath :: FilePath}
|
||||||
| GetRemoteFile {remoteHostId :: RemoteHostId, file :: RemoteFile}
|
| GetRemoteFile {remoteHostId :: RemoteHostId, file :: RemoteFile}
|
||||||
| ConnectRemoteCtrl RCSignedInvitation -- ^ Connect new or existing controller via OOB data
|
| ConnectRemoteCtrl RCSignedInvitation -- Connect new or existing controller via OOB data
|
||||||
| FindKnownRemoteCtrl -- ^ Start listening for announcements from all existing controllers
|
| FindKnownRemoteCtrl -- Start listening for announcements from all existing controllers
|
||||||
| ConfirmRemoteCtrl RemoteCtrlId -- ^ Confirm the connection with found controller
|
| ConfirmRemoteCtrl RemoteCtrlId -- Confirm the connection with found controller
|
||||||
| VerifyRemoteCtrlSession Text -- ^ Verify remote controller session
|
| VerifyRemoteCtrlSession Text -- Verify remote controller session
|
||||||
| ListRemoteCtrls
|
| ListRemoteCtrls
|
||||||
| StopRemoteCtrl -- ^ Stop listening for announcements or terminate an active session
|
| StopRemoteCtrl -- Stop listening for announcements or terminate an active session
|
||||||
| DeleteRemoteCtrl RemoteCtrlId -- ^ Remove all local data associated with a remote controller session
|
| DeleteRemoteCtrl RemoteCtrlId -- Remove all local data associated with a remote controller session
|
||||||
| QuitChat
|
| QuitChat
|
||||||
| ShowVersion
|
| ShowVersion
|
||||||
| DebugLocks
|
| DebugLocks
|
||||||
@@ -469,7 +470,7 @@ allowRemoteCommand = \case
|
|||||||
APIGetNetworkConfig -> False
|
APIGetNetworkConfig -> False
|
||||||
SetLocalDeviceName _ -> False
|
SetLocalDeviceName _ -> False
|
||||||
ListRemoteHosts -> False
|
ListRemoteHosts -> False
|
||||||
StartRemoteHost _ -> False
|
StartRemoteHost {} -> False
|
||||||
SwitchRemoteHost {} -> False
|
SwitchRemoteHost {} -> False
|
||||||
StoreRemoteFile {} -> False
|
StoreRemoteFile {} -> False
|
||||||
GetRemoteFile {} -> False
|
GetRemoteFile {} -> False
|
||||||
@@ -556,8 +557,8 @@ data ChatResponse
|
|||||||
| CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation, connection :: PendingContactConnection}
|
| CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation, connection :: PendingContactConnection}
|
||||||
| CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection}
|
| CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection}
|
||||||
| CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan}
|
| CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan}
|
||||||
| CRSentConfirmation {user :: User}
|
| CRSentConfirmation {user :: User, connection :: PendingContactConnection}
|
||||||
| CRSentInvitation {user :: User, customUserProfile :: Maybe Profile}
|
| CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile}
|
||||||
| CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile}
|
| CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile}
|
||||||
| CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
|
| CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
|
||||||
| CRGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember}
|
| CRGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember}
|
||||||
@@ -654,11 +655,10 @@ data ChatResponse
|
|||||||
| CRNtfTokenStatus {status :: NtfTknStatus}
|
| CRNtfTokenStatus {status :: NtfTknStatus}
|
||||||
| CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode}
|
| CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode}
|
||||||
| CRNtfMessages {user_ :: Maybe User, connEntity :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]}
|
| CRNtfMessages {user_ :: Maybe User, connEntity :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]}
|
||||||
| CRNewContactConnection {user :: User, connection :: PendingContactConnection}
|
|
||||||
| CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection}
|
| CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection}
|
||||||
| CRRemoteHostList {remoteHosts :: [RemoteHostInfo]}
|
| CRRemoteHostList {remoteHosts :: [RemoteHostInfo]}
|
||||||
| CRCurrentRemoteHost {remoteHost_ :: Maybe RemoteHostInfo}
|
| CRCurrentRemoteHost {remoteHost_ :: Maybe RemoteHostInfo}
|
||||||
| CRRemoteHostStarted {remoteHost_ :: Maybe RemoteHostInfo, invitation :: Text, ctrlPort :: String}
|
| CRRemoteHostStarted {remoteHost_ :: Maybe RemoteHostInfo, invitation :: Text, ctrlPort :: String, localAddrs :: NonEmpty RCCtrlAddress}
|
||||||
| CRRemoteHostSessionCode {remoteHost_ :: Maybe RemoteHostInfo, sessionCode :: Text}
|
| CRRemoteHostSessionCode {remoteHost_ :: Maybe RemoteHostInfo, sessionCode :: Text}
|
||||||
| CRNewRemoteHost {remoteHost :: RemoteHostInfo}
|
| CRNewRemoteHost {remoteHost :: RemoteHostInfo}
|
||||||
| CRRemoteHostConnected {remoteHost :: RemoteHostInfo}
|
| CRRemoteHostConnected {remoteHost :: RemoteHostInfo}
|
||||||
@@ -1072,32 +1072,33 @@ throwDBError = throwError . ChatErrorDatabase
|
|||||||
|
|
||||||
-- TODO review errors, some of it can be covered by HTTP2 errors
|
-- TODO review errors, some of it can be covered by HTTP2 errors
|
||||||
data RemoteHostError
|
data RemoteHostError
|
||||||
= RHEMissing -- ^ No remote session matches this identifier
|
= RHEMissing -- No remote session matches this identifier
|
||||||
| RHEInactive -- ^ A session exists, but not active
|
| RHEInactive -- A session exists, but not active
|
||||||
| RHEBusy -- ^ A session is already running
|
| RHEBusy -- A session is already running
|
||||||
| RHETimeout
|
| RHETimeout
|
||||||
| RHEBadState -- ^ Illegal state transition
|
| RHEBadState -- Illegal state transition
|
||||||
| RHEBadVersion {appVersion :: AppVersion}
|
| RHEBadVersion {appVersion :: AppVersion}
|
||||||
| RHELocalCommand -- ^ Command not allowed for remote execution
|
| RHELocalCommand -- Command not allowed for remote execution
|
||||||
| RHEDisconnected {reason :: Text} -- TODO should be sent when disconnected?
|
| RHEDisconnected {reason :: Text} -- TODO should be sent when disconnected?
|
||||||
| RHEProtocolError RemoteProtocolError
|
| RHEProtocolError RemoteProtocolError
|
||||||
deriving (Show, Exception)
|
deriving (Show, Exception)
|
||||||
|
|
||||||
data RemoteHostStopReason
|
data RemoteHostStopReason
|
||||||
= RHSRConnectionFailed ChatError
|
= RHSRConnectionFailed {chatError :: ChatError}
|
||||||
| RHSRCrashed ChatError
|
| RHSRCrashed {chatError :: ChatError}
|
||||||
| RHSRDisconnected
|
| RHSRDisconnected
|
||||||
deriving (Show, Exception)
|
deriving (Show, Exception)
|
||||||
|
|
||||||
-- TODO review errors, some of it can be covered by HTTP2 errors
|
-- TODO review errors, some of it can be covered by HTTP2 errors
|
||||||
data RemoteCtrlError
|
data RemoteCtrlError
|
||||||
= RCEInactive -- ^ No session is running
|
= RCEInactive -- No session is running
|
||||||
| RCEBadState -- ^ A session is in a wrong state for the current operation
|
| RCEBadState -- A session is in a wrong state for the current operation
|
||||||
| RCEBusy -- ^ A session is already running
|
| RCEBusy -- A session is already running
|
||||||
| RCETimeout
|
| RCETimeout
|
||||||
| RCENoKnownControllers -- ^ No previously-contacted controllers to discover
|
| RCENoKnownControllers -- No previously-contacted controllers to discover
|
||||||
| RCEBadController -- ^ Attempting to confirm a found controller with another ID
|
| RCEBadController -- Attempting to confirm a found controller with another ID
|
||||||
| RCEDisconnected {remoteCtrlId :: RemoteCtrlId, reason :: Text} -- ^ A session disconnected by a controller
|
| -- | A session disconnected by a controller
|
||||||
|
RCEDisconnected {remoteCtrlId :: RemoteCtrlId, reason :: Text}
|
||||||
| RCEBadInvitation
|
| RCEBadInvitation
|
||||||
| RCEBadVersion {appVersion :: AppVersion}
|
| RCEBadVersion {appVersion :: AppVersion}
|
||||||
| RCEHTTP2Error {http2Error :: Text} -- TODO currently not used
|
| RCEHTTP2Error {http2Error :: Text} -- TODO currently not used
|
||||||
@@ -1105,9 +1106,9 @@ data RemoteCtrlError
|
|||||||
deriving (Show, Exception)
|
deriving (Show, Exception)
|
||||||
|
|
||||||
data RemoteCtrlStopReason
|
data RemoteCtrlStopReason
|
||||||
= RCSRDiscoveryFailed ChatError
|
= RCSRDiscoveryFailed {chatError :: ChatError}
|
||||||
| RCSRConnectionFailed ChatError
|
| RCSRConnectionFailed {chatError :: ChatError}
|
||||||
| RCSRSetupFailed ChatError
|
| RCSRSetupFailed {chatError :: ChatError}
|
||||||
| RCSRDisconnected
|
| RCSRDisconnected
|
||||||
deriving (Show, Exception)
|
deriving (Show, Exception)
|
||||||
|
|
||||||
@@ -1223,8 +1224,8 @@ toView event = do
|
|||||||
session <- asks remoteCtrlSession
|
session <- asks remoteCtrlSession
|
||||||
atomically $
|
atomically $
|
||||||
readTVar session >>= \case
|
readTVar session >>= \case
|
||||||
Just (_, RCSessionConnected {remoteOutputQ}) | allowRemoteEvent event ->
|
Just (_, RCSessionConnected {remoteOutputQ})
|
||||||
writeTBQueue remoteOutputQ event
|
| allowRemoteEvent event -> writeTBQueue remoteOutputQ event
|
||||||
-- TODO potentially, it should hold some events while connecting
|
-- TODO potentially, it should hold some events while connecting
|
||||||
_ -> writeTBQueue localQ (Nothing, Nothing, event)
|
_ -> writeTBQueue localQ (Nothing, Nothing, event)
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController
|
|||||||
runSimplexChat ChatOpts {maintenance} u cc chat
|
runSimplexChat ChatOpts {maintenance} u cc chat
|
||||||
| maintenance = wait =<< async (chat u cc)
|
| maintenance = wait =<< async (chat u cc)
|
||||||
| otherwise = do
|
| otherwise = do
|
||||||
a1 <- runReaderT (startChatController True True True) cc
|
a1 <- runReaderT (startChatController True True True) cc
|
||||||
a2 <- async $ chat u cc
|
a2 <- async $ chat u cc
|
||||||
waitEither_ a1 a2
|
waitEither_ a1 a2
|
||||||
|
|
||||||
sendChatCmdStr :: ChatController -> String -> IO ChatResponse
|
sendChatCmdStr :: ChatController -> String -> IO ChatResponse
|
||||||
sendChatCmdStr cc s = runReaderT (execChatCommand Nothing . encodeUtf8 $ T.pack s) cc
|
sendChatCmdStr cc s = runReaderT (execChatCommand Nothing . encodeUtf8 $ T.pack s) cc
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ module Simplex.Chat.Files where
|
|||||||
import Control.Monad.IO.Class
|
import Control.Monad.IO.Class
|
||||||
import Simplex.Chat.Controller
|
import Simplex.Chat.Controller
|
||||||
import Simplex.Messaging.Util (ifM)
|
import Simplex.Messaging.Util (ifM)
|
||||||
import System.FilePath (splitExtensions, combine)
|
import System.FilePath (combine, splitExtensions)
|
||||||
import UnliftIO.Directory (doesFileExist, getTemporaryDirectory, getHomeDirectory, doesDirectoryExist)
|
import UnliftIO.Directory (doesDirectoryExist, doesFileExist, getHomeDirectory, getTemporaryDirectory)
|
||||||
|
|
||||||
uniqueCombine :: MonadIO m => FilePath -> String -> m FilePath
|
uniqueCombine :: MonadIO m => FilePath -> String -> m FilePath
|
||||||
uniqueCombine fPath fName = tryCombine (0 :: Int)
|
uniqueCombine fPath fName = tryCombine (0 :: Int)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import qualified Data.Attoparsec.Text as A
|
|||||||
import Data.Char (isDigit, isPunctuation)
|
import Data.Char (isDigit, isPunctuation)
|
||||||
import Data.Either (fromRight)
|
import Data.Either (fromRight)
|
||||||
import Data.Functor (($>))
|
import Data.Functor (($>))
|
||||||
import Data.List (intercalate, foldl')
|
import Data.List (foldl', intercalate)
|
||||||
import Data.List.NonEmpty (NonEmpty)
|
import Data.List.NonEmpty (NonEmpty)
|
||||||
import qualified Data.List.NonEmpty as L
|
import qualified Data.List.NonEmpty as L
|
||||||
import Data.Maybe (fromMaybe, isNothing)
|
import Data.Maybe (fromMaybe, isNothing)
|
||||||
@@ -85,16 +85,18 @@ newtype FormatColor = FormatColor Color
|
|||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
instance FromJSON FormatColor where
|
instance FromJSON FormatColor where
|
||||||
parseJSON = J.withText "FormatColor" $ fmap FormatColor . \case
|
parseJSON =
|
||||||
"red" -> pure Red
|
J.withText "FormatColor" $
|
||||||
"green" -> pure Green
|
fmap FormatColor . \case
|
||||||
"blue" -> pure Blue
|
"red" -> pure Red
|
||||||
"yellow" -> pure Yellow
|
"green" -> pure Green
|
||||||
"cyan" -> pure Cyan
|
"blue" -> pure Blue
|
||||||
"magenta" -> pure Magenta
|
"yellow" -> pure Yellow
|
||||||
"black" -> pure Black
|
"cyan" -> pure Cyan
|
||||||
"white" -> pure White
|
"magenta" -> pure Magenta
|
||||||
unexpected -> fail $ "unexpected FormatColor: " <> show unexpected
|
"black" -> pure Black
|
||||||
|
"white" -> pure White
|
||||||
|
unexpected -> fail $ "unexpected FormatColor: " <> show unexpected
|
||||||
|
|
||||||
instance ToJSON FormatColor where
|
instance ToJSON FormatColor where
|
||||||
toJSON (FormatColor c) = case c of
|
toJSON (FormatColor c) = case c of
|
||||||
@@ -167,14 +169,14 @@ markdownP = mconcat <$> A.many' fragmentP
|
|||||||
md :: Char -> Format -> Text -> Markdown
|
md :: Char -> Format -> Text -> Markdown
|
||||||
md c f s
|
md c f s
|
||||||
| T.null s || T.head s == ' ' || T.last s == ' ' =
|
| T.null s || T.head s == ' ' || T.last s == ' ' =
|
||||||
unmarked $ c `T.cons` s `T.snoc` c
|
unmarked $ c `T.cons` s `T.snoc` c
|
||||||
| otherwise = markdown f s
|
| otherwise = markdown f s
|
||||||
secretP :: Parser Markdown
|
secretP :: Parser Markdown
|
||||||
secretP = secret <$> A.takeWhile (== '#') <*> A.takeTill (== '#') <*> A.takeWhile (== '#')
|
secretP = secret <$> A.takeWhile (== '#') <*> A.takeTill (== '#') <*> A.takeWhile (== '#')
|
||||||
secret :: Text -> Text -> Text -> Markdown
|
secret :: Text -> Text -> Text -> Markdown
|
||||||
secret b s a
|
secret b s a
|
||||||
| T.null a || T.null s || T.head s == ' ' || T.last s == ' ' =
|
| T.null a || T.null s || T.head s == ' ' || T.last s == ' ' =
|
||||||
unmarked $ '#' `T.cons` ss
|
unmarked $ '#' `T.cons` ss
|
||||||
| otherwise = markdown Secret $ T.init ss
|
| otherwise = markdown Secret $ T.init ss
|
||||||
where
|
where
|
||||||
ss = b <> s <> a
|
ss = b <> s <> a
|
||||||
@@ -215,9 +217,9 @@ markdownP = mconcat <$> A.many' fragmentP
|
|||||||
wordMD s
|
wordMD s
|
||||||
| T.null s = unmarked s
|
| T.null s = unmarked s
|
||||||
| isUri s =
|
| isUri s =
|
||||||
let t = T.takeWhileEnd isPunctuation s
|
let t = T.takeWhileEnd isPunctuation s
|
||||||
uri = uriMarkdown $ T.dropWhileEnd isPunctuation s
|
uri = uriMarkdown $ T.dropWhileEnd isPunctuation s
|
||||||
in if T.null t then uri else uri :|: unmarked t
|
in if T.null t then uri else uri :|: unmarked t
|
||||||
| isEmail s = markdown Email s
|
| isEmail s = markdown Email s
|
||||||
| otherwise = unmarked s
|
| otherwise = unmarked s
|
||||||
uriMarkdown s = case strDecode $ encodeUtf8 s of
|
uriMarkdown s = case strDecode $ encodeUtf8 s of
|
||||||
|
|||||||
@@ -5,13 +5,11 @@
|
|||||||
{-# LANGUAGE KindSignatures #-}
|
{-# LANGUAGE KindSignatures #-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE OverloadedRecordDot #-}
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE StandaloneDeriving #-}
|
{-# LANGUAGE StandaloneDeriving #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||||
|
|
||||||
module Simplex.Chat.Messages where
|
module Simplex.Chat.Messages where
|
||||||
@@ -44,7 +42,7 @@ import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptSta
|
|||||||
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
||||||
import qualified Simplex.Messaging.Crypto.File as CF
|
import qualified Simplex.Messaging.Crypto.File as CF
|
||||||
import Simplex.Messaging.Encoding.String
|
import Simplex.Messaging.Encoding.String
|
||||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, parseAll, enumJSON, sumTypeJSON)
|
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, parseAll, sumTypeJSON)
|
||||||
import Simplex.Messaging.Protocol (MsgBody)
|
import Simplex.Messaging.Protocol (MsgBody)
|
||||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
|
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
|
||||||
|
|
||||||
@@ -345,7 +343,7 @@ contactTimedTTL Contact {mergedPreferences = ContactUserPreferences {timedMessag
|
|||||||
| forUser enabled && forContact enabled = Just ttl
|
| forUser enabled && forContact enabled = Just ttl
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
where
|
where
|
||||||
TimedMessagesPreference {ttl} = userPreference.preference
|
TimedMessagesPreference {ttl} = preference (userPreference :: ContactUserPref TimedMessagesPreference)
|
||||||
|
|
||||||
groupTimedTTL :: GroupInfo -> Maybe (Maybe Int)
|
groupTimedTTL :: GroupInfo -> Maybe (Maybe Int)
|
||||||
groupTimedTTL GroupInfo {fullGroupPreferences = FullGroupPreferences {timedMessages = TimedMessagesGroupPreference {enable, ttl}}}
|
groupTimedTTL GroupInfo {fullGroupPreferences = FullGroupPreferences {timedMessages = TimedMessagesGroupPreference {enable, ttl}}}
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ profileToText Profile {displayName, fullName} = displayName <> optionalFullName
|
|||||||
msgIntegrityError :: MsgErrorType -> Text
|
msgIntegrityError :: MsgErrorType -> Text
|
||||||
msgIntegrityError = \case
|
msgIntegrityError = \case
|
||||||
MsgSkipped fromId toId ->
|
MsgSkipped fromId toId ->
|
||||||
"skipped message ID " <> tshow fromId
|
("skipped message ID " <> tshow fromId)
|
||||||
<> if fromId == toId then "" else ".." <> tshow toId
|
<> if fromId == toId then "" else ".." <> tshow toId
|
||||||
MsgBadId msgId -> "unexpected message ID " <> tshow msgId
|
MsgBadId msgId -> "unexpected message ID " <> tshow msgId
|
||||||
MsgBadHash -> "incorrect message hash"
|
MsgBadHash -> "incorrect message hash"
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ data SndConnEvent
|
|||||||
| SCERatchetSync {syncStatus :: RatchetSyncState, member :: Maybe GroupMemberRef}
|
| SCERatchetSync {syncStatus :: RatchetSyncState, member :: Maybe GroupMemberRef}
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
data RcvDirectEvent =
|
data RcvDirectEvent
|
||||||
-- RDEProfileChanged {...}
|
= -- RDEProfileChanged {...}
|
||||||
RDEContactDeleted
|
RDEContactDeleted
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
-- platform-specific JSON encoding (used in API)
|
-- platform-specific JSON encoding (used in API)
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
|
module Simplex.Chat.Migrations.M20231126_remote_ctrl_address where
|
||||||
|
|
||||||
|
import Database.SQLite.Simple (Query)
|
||||||
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
|
|
||||||
|
m20231126_remote_ctrl_address :: Query
|
||||||
|
m20231126_remote_ctrl_address =
|
||||||
|
[sql|
|
||||||
|
ALTER TABLE remote_hosts ADD COLUMN bind_addr TEXT;
|
||||||
|
ALTER TABLE remote_hosts ADD COLUMN bind_iface TEXT;
|
||||||
|
ALTER TABLE remote_hosts ADD COLUMN bind_port INTEGER;
|
||||||
|
|]
|
||||||
|
|
||||||
|
down_m20231126_remote_ctrl_address :: Query
|
||||||
|
down_m20231126_remote_ctrl_address =
|
||||||
|
[sql|
|
||||||
|
ALTER TABLE remote_hosts DROP COLUMN bind_addr;
|
||||||
|
ALTER TABLE remote_hosts DROP COLUMN bind_iface;
|
||||||
|
ALTER TABLE remote_hosts DROP COLUMN bind_port;
|
||||||
|
|]
|
||||||
@@ -537,6 +537,10 @@ CREATE TABLE remote_hosts(
|
|||||||
id_key BLOB NOT NULL, -- long-term/identity signing key
|
id_key BLOB NOT NULL, -- long-term/identity signing key
|
||||||
host_fingerprint BLOB NOT NULL, -- remote host CA cert fingerprint, set when connected
|
host_fingerprint BLOB NOT NULL, -- remote host CA cert fingerprint, set when connected
|
||||||
host_dh_pub BLOB NOT NULL -- last session DH key
|
host_dh_pub BLOB NOT NULL -- last session DH key
|
||||||
|
,
|
||||||
|
bind_addr TEXT,
|
||||||
|
bind_iface TEXT,
|
||||||
|
bind_port INTEGER
|
||||||
);
|
);
|
||||||
CREATE TABLE remote_controllers(
|
CREATE TABLE remote_controllers(
|
||||||
-- e.g., desktops known to a mobile app
|
-- e.g., desktops known to a mobile app
|
||||||
|
|||||||
@@ -4,13 +4,12 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fobject-code #-}
|
{-# OPTIONS_GHC -fobject-code #-}
|
||||||
|
|
||||||
module Simplex.Chat.Mobile where
|
module Simplex.Chat.Mobile where
|
||||||
|
|
||||||
import Control.Concurrent.STM
|
import Control.Concurrent.STM
|
||||||
import Control.Exception (catch, SomeException)
|
import Control.Exception (SomeException, catch)
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
import Control.Monad.Reader
|
import Control.Monad.Reader
|
||||||
import qualified Data.Aeson as J
|
import qualified Data.Aeson as J
|
||||||
@@ -31,7 +30,7 @@ import Foreign.C.Types (CInt (..))
|
|||||||
import Foreign.Ptr
|
import Foreign.Ptr
|
||||||
import Foreign.StablePtr
|
import Foreign.StablePtr
|
||||||
import Foreign.Storable (poke)
|
import Foreign.Storable (poke)
|
||||||
import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding)
|
import GHC.IO.Encoding (setFileSystemEncoding, setForeignEncoding, setLocaleEncoding)
|
||||||
import Simplex.Chat
|
import Simplex.Chat
|
||||||
import Simplex.Chat.Controller
|
import Simplex.Chat.Controller
|
||||||
import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList)
|
import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList)
|
||||||
@@ -219,7 +218,7 @@ chatMigrateInit dbFilePrefix dbKey confirm = runExceptT $ do
|
|||||||
ExceptT $
|
ExceptT $
|
||||||
(first (DBMErrorMigration dbFile) <$> createStore dbFile dbKey confirmMigrations)
|
(first (DBMErrorMigration dbFile) <$> createStore dbFile dbKey confirmMigrations)
|
||||||
`catch` (pure . checkDBError)
|
`catch` (pure . checkDBError)
|
||||||
`catchAll` (pure . dbError)
|
`catchAll` (pure . dbError)
|
||||||
where
|
where
|
||||||
checkDBError e = case sqlError e of
|
checkDBError e = case sqlError e of
|
||||||
DB.ErrorNotADatabase -> Left $ DBMErrorNotADatabase dbFile
|
DB.ErrorNotADatabase -> Left $ DBMErrorNotADatabase dbFile
|
||||||
@@ -233,7 +232,7 @@ chatCloseStore ChatController {chatStore, smpAgent} = handleErr $ do
|
|||||||
|
|
||||||
handleErr :: IO () -> IO String
|
handleErr :: IO () -> IO String
|
||||||
handleErr a = (a $> "") `catch` (pure . show @SomeException)
|
handleErr a = (a $> "") `catch` (pure . show @SomeException)
|
||||||
|
|
||||||
chatSendCmd :: ChatController -> B.ByteString -> IO JSONByteString
|
chatSendCmd :: ChatController -> B.ByteString -> IO JSONByteString
|
||||||
chatSendCmd cc = chatSendRemoteCmd cc Nothing
|
chatSendCmd cc = chatSendRemoteCmd cc Nothing
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import qualified Data.ByteString as B
|
|||||||
import Data.ByteString.Internal (ByteString (..), memcpy)
|
import Data.ByteString.Internal (ByteString (..), memcpy)
|
||||||
import qualified Data.ByteString.Lazy as LB
|
import qualified Data.ByteString.Lazy as LB
|
||||||
import qualified Data.ByteString.Lazy.Internal as LB
|
import qualified Data.ByteString.Lazy.Internal as LB
|
||||||
import Foreign.C (CInt, CString)
|
|
||||||
import Foreign
|
import Foreign
|
||||||
|
import Foreign.C (CInt, CString)
|
||||||
|
|
||||||
type CJSONString = CString
|
type CJSONString = CString
|
||||||
|
|
||||||
@@ -16,12 +16,12 @@ type JSONByteString = LB.ByteString
|
|||||||
getByteString :: Ptr Word8 -> CInt -> IO ByteString
|
getByteString :: Ptr Word8 -> CInt -> IO ByteString
|
||||||
getByteString ptr len = do
|
getByteString ptr len = do
|
||||||
fp <- newForeignPtr_ ptr
|
fp <- newForeignPtr_ ptr
|
||||||
pure $ BS fp $ fromIntegral len
|
pure $ PS fp 0 $ fromIntegral len
|
||||||
{-# INLINE getByteString #-}
|
{-# INLINE getByteString #-}
|
||||||
|
|
||||||
putByteString :: Ptr Word8 -> ByteString -> IO ()
|
putByteString :: Ptr Word8 -> ByteString -> IO ()
|
||||||
putByteString ptr (BS fp len) =
|
putByteString ptr (PS fp offset len) =
|
||||||
withForeignPtr fp $ \p -> memcpy ptr p len
|
withForeignPtr fp $ \p -> memcpy ptr (p `plusPtr` offset) len
|
||||||
{-# INLINE putByteString #-}
|
{-# INLINE putByteString #-}
|
||||||
|
|
||||||
putLazyByteString :: Ptr Word8 -> LB.ByteString -> IO ()
|
putLazyByteString :: Ptr Word8 -> LB.ByteString -> IO ()
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
|
||||||
module Simplex.Chat.Mobile.WebRTC (
|
module Simplex.Chat.Mobile.WebRTC
|
||||||
cChatEncryptMedia,
|
( cChatEncryptMedia,
|
||||||
cChatDecryptMedia,
|
cChatDecryptMedia,
|
||||||
chatEncryptMedia,
|
chatEncryptMedia,
|
||||||
chatDecryptMedia,
|
chatDecryptMedia,
|
||||||
reservedSize,
|
reservedSize,
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
@@ -21,8 +21,8 @@ import Data.Either (fromLeft)
|
|||||||
import Data.Word (Word8)
|
import Data.Word (Word8)
|
||||||
import Foreign.C (CInt, CString, newCAString)
|
import Foreign.C (CInt, CString, newCAString)
|
||||||
import Foreign.Ptr (Ptr)
|
import Foreign.Ptr (Ptr)
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
|
||||||
import Simplex.Chat.Mobile.Shared
|
import Simplex.Chat.Mobile.Shared
|
||||||
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
|
|
||||||
cChatEncryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
|
cChatEncryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
|
||||||
cChatEncryptMedia = cTransformMedia chatEncryptMedia
|
cChatEncryptMedia = cTransformMedia chatEncryptMedia
|
||||||
|
|||||||
@@ -206,7 +206,6 @@ chatOptsP appDir defaultDbFileName = do
|
|||||||
optional $
|
optional $
|
||||||
strOption
|
strOption
|
||||||
( long "device-name"
|
( long "device-name"
|
||||||
<> short 'e'
|
|
||||||
<> metavar "DEVICE"
|
<> metavar "DEVICE"
|
||||||
<> help "Device name to use in connections with remote hosts and controller"
|
<> help "Device name to use in connections with remote hosts and controller"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ generateRandomProfile = do
|
|||||||
pickNoun adjective n
|
pickNoun adjective n
|
||||||
| n == 0 = pick nouns
|
| n == 0 = pick nouns
|
||||||
| otherwise = do
|
| otherwise = do
|
||||||
noun <- pick nouns
|
noun <- pick nouns
|
||||||
if noun == adjective
|
if noun == adjective
|
||||||
then pickNoun adjective (n - 1)
|
then pickNoun adjective (n - 1)
|
||||||
else pure noun
|
else pure noun
|
||||||
|
|
||||||
adjectives :: [Text]
|
adjectives :: [Text]
|
||||||
adjectives =
|
adjectives =
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
{-# LANGUAGE StrictData #-}
|
{-# LANGUAGE StrictData #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||||
|
|
||||||
module Simplex.Chat.Protocol where
|
module Simplex.Chat.Protocol where
|
||||||
|
|||||||
+61
-51
@@ -26,13 +26,14 @@ import qualified Data.ByteString.Base64.URL as B64U
|
|||||||
import Data.ByteString.Builder (Builder)
|
import Data.ByteString.Builder (Builder)
|
||||||
import qualified Data.ByteString.Char8 as B
|
import qualified Data.ByteString.Char8 as B
|
||||||
import Data.Functor (($>))
|
import Data.Functor (($>))
|
||||||
import Data.List.NonEmpty (nonEmpty)
|
import Data.List.NonEmpty (NonEmpty, nonEmpty)
|
||||||
|
import qualified Data.List.NonEmpty as L
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
import Data.Maybe (fromMaybe, isJust)
|
import Data.Maybe (fromMaybe, isJust)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||||
import Data.Word (Word32)
|
import Data.Word (Word16, Word32)
|
||||||
import qualified Network.HTTP.Types as N
|
import qualified Network.HTTP.Types as N
|
||||||
import Network.HTTP2.Server (responseStreaming)
|
import Network.HTTP2.Server (responseStreaming)
|
||||||
import qualified Paths_simplex_chat as SC
|
import qualified Paths_simplex_chat as SC
|
||||||
@@ -97,24 +98,26 @@ discoveryTimeout = 60000000
|
|||||||
getRemoteHostClient :: ChatMonad m => RemoteHostId -> m RemoteHostClient
|
getRemoteHostClient :: ChatMonad m => RemoteHostId -> m RemoteHostClient
|
||||||
getRemoteHostClient rhId = do
|
getRemoteHostClient rhId = do
|
||||||
sessions <- asks remoteHostSessions
|
sessions <- asks remoteHostSessions
|
||||||
liftIOEither . atomically $ TM.lookup rhKey sessions >>= \case
|
liftIOEither . atomically $
|
||||||
Just (_, RHSessionConnected {rhClient}) -> pure $ Right rhClient
|
TM.lookup rhKey sessions >>= \case
|
||||||
Just _ -> pure . Left $ ChatErrorRemoteHost rhKey RHEBadState
|
Just (_, RHSessionConnected {rhClient}) -> pure $ Right rhClient
|
||||||
Nothing -> pure . Left $ ChatErrorRemoteHost rhKey RHEMissing
|
Just _ -> pure . Left $ ChatErrorRemoteHost rhKey RHEBadState
|
||||||
|
Nothing -> pure . Left $ ChatErrorRemoteHost rhKey RHEMissing
|
||||||
where
|
where
|
||||||
rhKey = RHId rhId
|
rhKey = RHId rhId
|
||||||
|
|
||||||
withRemoteHostSession :: ChatMonad m => RHKey -> SessionSeq -> (RemoteHostSession -> Either ChatError (a, RemoteHostSession)) -> m a
|
withRemoteHostSession :: ChatMonad m => RHKey -> SessionSeq -> (RemoteHostSession -> Either ChatError (a, RemoteHostSession)) -> m a
|
||||||
withRemoteHostSession rhKey sseq f = do
|
withRemoteHostSession rhKey sseq f = do
|
||||||
sessions <- asks remoteHostSessions
|
sessions <- asks remoteHostSessions
|
||||||
r <- atomically $
|
r <-
|
||||||
TM.lookup rhKey sessions >>= \case
|
atomically $
|
||||||
Nothing -> pure . Left $ ChatErrorRemoteHost rhKey RHEMissing
|
TM.lookup rhKey sessions >>= \case
|
||||||
Just (stateSeq, state)
|
Nothing -> pure . Left $ ChatErrorRemoteHost rhKey RHEMissing
|
||||||
| stateSeq /= sseq -> pure . Left $ ChatErrorRemoteHost rhKey RHEBadState
|
Just (stateSeq, state)
|
||||||
| otherwise -> case f state of
|
| stateSeq /= sseq -> pure . Left $ ChatErrorRemoteHost rhKey RHEBadState
|
||||||
Right (r, newState) -> Right r <$ TM.insert rhKey (sseq, newState) sessions
|
| otherwise -> case f state of
|
||||||
Left ce -> pure $ Left ce
|
Right (r, newState) -> Right r <$ TM.insert rhKey (sseq, newState) sessions
|
||||||
|
Left ce -> pure $ Left ce
|
||||||
liftEither r
|
liftEither r
|
||||||
|
|
||||||
-- | Transition session state with a 'RHNew' ID to an assigned 'RemoteHostId'
|
-- | Transition session state with a 'RHNew' ID to an assigned 'RemoteHostId'
|
||||||
@@ -133,8 +136,8 @@ setNewRemoteHostId sseq rhId = do
|
|||||||
where
|
where
|
||||||
err = pure . Left . ChatErrorRemoteHost RHNew
|
err = pure . Left . ChatErrorRemoteHost RHNew
|
||||||
|
|
||||||
startRemoteHost :: ChatMonad m => Maybe (RemoteHostId, Bool) -> m (Maybe RemoteHostInfo, RCSignedInvitation)
|
startRemoteHost :: ChatMonad m => Maybe (RemoteHostId, Bool) -> Maybe RCCtrlAddress -> Maybe Word16 -> m (NonEmpty RCCtrlAddress, Maybe RemoteHostInfo, RCSignedInvitation)
|
||||||
startRemoteHost rh_ = do
|
startRemoteHost rh_ rcAddrPrefs_ port_ = do
|
||||||
(rhKey, multicast, remoteHost_, pairing) <- case rh_ of
|
(rhKey, multicast, remoteHost_, pairing) <- case rh_ of
|
||||||
Just (rhId, multicast) -> do
|
Just (rhId, multicast) -> do
|
||||||
rh@RemoteHost {hostPairing} <- withStore $ \db -> getRemoteHost db rhId
|
rh@RemoteHost {hostPairing} <- withStore $ \db -> getRemoteHost db rhId
|
||||||
@@ -142,19 +145,20 @@ startRemoteHost rh_ = do
|
|||||||
Nothing -> (RHNew,False,Nothing,) <$> rcNewHostPairing
|
Nothing -> (RHNew,False,Nothing,) <$> rcNewHostPairing
|
||||||
sseq <- startRemoteHostSession rhKey
|
sseq <- startRemoteHostSession rhKey
|
||||||
ctrlAppInfo <- mkCtrlAppInfo
|
ctrlAppInfo <- mkCtrlAppInfo
|
||||||
(invitation, rchClient, vars) <- handleConnectError rhKey sseq . withAgent $ \a -> rcConnectHost a pairing (J.toJSON ctrlAppInfo) multicast
|
(localAddrs, invitation, rchClient, vars) <- handleConnectError rhKey sseq . withAgent $ \a -> rcConnectHost a pairing (J.toJSON ctrlAppInfo) multicast rcAddrPrefs_ port_
|
||||||
|
let rcAddr_ = L.head localAddrs <$ rcAddrPrefs_
|
||||||
cmdOk <- newEmptyTMVarIO
|
cmdOk <- newEmptyTMVarIO
|
||||||
rhsWaitSession <- async $ do
|
rhsWaitSession <- async $ do
|
||||||
rhKeyVar <- newTVarIO rhKey
|
rhKeyVar <- newTVarIO rhKey
|
||||||
atomically $ takeTMVar cmdOk
|
atomically $ takeTMVar cmdOk
|
||||||
handleHostError sseq rhKeyVar $ waitForHostSession remoteHost_ rhKey sseq rhKeyVar vars
|
handleHostError sseq rhKeyVar $ waitForHostSession remoteHost_ rhKey sseq rcAddr_ rhKeyVar vars
|
||||||
let rhs = RHPendingSession {rhKey, rchClient, rhsWaitSession, remoteHost_}
|
let rhs = RHPendingSession {rhKey, rchClient, rhsWaitSession, remoteHost_}
|
||||||
withRemoteHostSession rhKey sseq $ \case
|
withRemoteHostSession rhKey sseq $ \case
|
||||||
RHSessionStarting ->
|
RHSessionStarting ->
|
||||||
let inv = decodeLatin1 $ strEncode invitation
|
let inv = decodeLatin1 $ strEncode invitation
|
||||||
in Right ((), RHSessionConnecting inv rhs)
|
in Right ((), RHSessionConnecting inv rhs)
|
||||||
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
||||||
(remoteHost_, invitation) <$ atomically (putTMVar cmdOk ())
|
(localAddrs, remoteHost_, invitation) <$ atomically (putTMVar cmdOk ())
|
||||||
where
|
where
|
||||||
mkCtrlAppInfo = do
|
mkCtrlAppInfo = do
|
||||||
deviceName <- chatReadVar localDeviceName
|
deviceName <- chatReadVar localDeviceName
|
||||||
@@ -167,16 +171,18 @@ startRemoteHost rh_ = do
|
|||||||
when (encoding == PEKotlin && localEncoding == PESwift) $ throwError $ RHEProtocolError RPEIncompatibleEncoding
|
when (encoding == PEKotlin && localEncoding == PESwift) $ throwError $ RHEProtocolError RPEIncompatibleEncoding
|
||||||
pure hostInfo
|
pure hostInfo
|
||||||
handleConnectError :: ChatMonad m => RHKey -> SessionSeq -> m a -> m a
|
handleConnectError :: ChatMonad m => RHKey -> SessionSeq -> m a -> m a
|
||||||
handleConnectError rhKey sessSeq action = action `catchChatError` \err -> do
|
handleConnectError rhKey sessSeq action =
|
||||||
logError $ "startRemoteHost.rcConnectHost crashed: " <> tshow err
|
action `catchChatError` \err -> do
|
||||||
cancelRemoteHostSession (Just (sessSeq, RHSRConnectionFailed err)) rhKey
|
logError $ "startRemoteHost.rcConnectHost crashed: " <> tshow err
|
||||||
throwError err
|
cancelRemoteHostSession (Just (sessSeq, RHSRConnectionFailed err)) rhKey
|
||||||
|
throwError err
|
||||||
handleHostError :: ChatMonad m => SessionSeq -> TVar RHKey -> m () -> m ()
|
handleHostError :: ChatMonad m => SessionSeq -> TVar RHKey -> m () -> m ()
|
||||||
handleHostError sessSeq rhKeyVar action = action `catchChatError` \err -> do
|
handleHostError sessSeq rhKeyVar action =
|
||||||
logError $ "startRemoteHost.waitForHostSession crashed: " <> tshow err
|
action `catchChatError` \err -> do
|
||||||
readTVarIO rhKeyVar >>= cancelRemoteHostSession (Just (sessSeq, RHSRCrashed err))
|
logError $ "startRemoteHost.waitForHostSession crashed: " <> tshow err
|
||||||
waitForHostSession :: ChatMonad m => Maybe RemoteHostInfo -> RHKey -> SessionSeq -> TVar RHKey -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> m ()
|
readTVarIO rhKeyVar >>= cancelRemoteHostSession (Just (sessSeq, RHSRCrashed err))
|
||||||
waitForHostSession remoteHost_ rhKey sseq rhKeyVar vars = do
|
waitForHostSession :: ChatMonad m => Maybe RemoteHostInfo -> RHKey -> SessionSeq -> Maybe RCCtrlAddress -> TVar RHKey -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> m ()
|
||||||
|
waitForHostSession remoteHost_ rhKey sseq rcAddr_ rhKeyVar vars = do
|
||||||
(sessId, tls, vars') <- timeoutThrow (ChatErrorRemoteHost rhKey RHETimeout) 60000000 $ takeRCStep vars
|
(sessId, tls, vars') <- timeoutThrow (ChatErrorRemoteHost rhKey RHETimeout) 60000000 $ takeRCStep vars
|
||||||
let sessionCode = verificationCode sessId
|
let sessionCode = verificationCode sessId
|
||||||
withRemoteHostSession rhKey sseq $ \case
|
withRemoteHostSession rhKey sseq $ \case
|
||||||
@@ -190,7 +196,7 @@ startRemoteHost rh_ = do
|
|||||||
withRemoteHostSession rhKey sseq $ \case
|
withRemoteHostSession rhKey sseq $ \case
|
||||||
RHSessionPendingConfirmation _ tls' rhs' -> Right ((), RHSessionConfirmed tls' rhs')
|
RHSessionPendingConfirmation _ tls' rhs' -> Right ((), RHSessionConfirmed tls' rhs')
|
||||||
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
||||||
rhi@RemoteHostInfo {remoteHostId, storePath} <- upsertRemoteHost pairing' rh_' hostDeviceName sseq RHSConfirmed {sessionCode}
|
rhi@RemoteHostInfo {remoteHostId, storePath} <- upsertRemoteHost pairing' rh_' rcAddr_ hostDeviceName sseq RHSConfirmed {sessionCode}
|
||||||
let rhKey' = RHId remoteHostId -- rhKey may be invalid after upserting on RHNew
|
let rhKey' = RHId remoteHostId -- rhKey may be invalid after upserting on RHNew
|
||||||
when (rhKey' /= rhKey) $ do
|
when (rhKey' /= rhKey) $ do
|
||||||
atomically $ writeTVar rhKeyVar rhKey'
|
atomically $ writeTVar rhKeyVar rhKey'
|
||||||
@@ -205,17 +211,17 @@ startRemoteHost rh_ = do
|
|||||||
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
||||||
chatWriteVar currentRemoteHost $ Just remoteHostId -- this is required for commands to be passed to remote host
|
chatWriteVar currentRemoteHost $ Just remoteHostId -- this is required for commands to be passed to remote host
|
||||||
toView $ CRRemoteHostConnected rhi {sessionState = Just RHSConnected {sessionCode}}
|
toView $ CRRemoteHostConnected rhi {sessionState = Just RHSConnected {sessionCode}}
|
||||||
upsertRemoteHost :: ChatMonad m => RCHostPairing -> Maybe RemoteHostInfo -> Text -> SessionSeq -> RemoteHostSessionState -> m RemoteHostInfo
|
upsertRemoteHost :: ChatMonad m => RCHostPairing -> Maybe RemoteHostInfo -> Maybe RCCtrlAddress -> Text -> SessionSeq -> RemoteHostSessionState -> m RemoteHostInfo
|
||||||
upsertRemoteHost pairing'@RCHostPairing {knownHost = kh_} rhi_ hostDeviceName sseq state = do
|
upsertRemoteHost pairing'@RCHostPairing {knownHost = kh_} rhi_ rcAddr_ hostDeviceName sseq state = do
|
||||||
KnownHostPairing {hostDhPubKey = hostDhPubKey'} <- maybe (throwError . ChatError $ CEInternalError "KnownHost is known after verification") pure kh_
|
KnownHostPairing {hostDhPubKey = hostDhPubKey'} <- maybe (throwError . ChatError $ CEInternalError "KnownHost is known after verification") pure kh_
|
||||||
case rhi_ of
|
case rhi_ of
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
storePath <- liftIO randomStorePath
|
storePath <- liftIO randomStorePath
|
||||||
rh@RemoteHost {remoteHostId} <- withStore $ \db -> insertRemoteHost db hostDeviceName storePath pairing' >>= getRemoteHost db
|
rh@RemoteHost {remoteHostId} <- withStore $ \db -> insertRemoteHost db hostDeviceName storePath rcAddr_ port_ pairing' >>= getRemoteHost db
|
||||||
setNewRemoteHostId sseq remoteHostId
|
setNewRemoteHostId sseq remoteHostId
|
||||||
pure $ remoteHostInfo rh $ Just state
|
pure $ remoteHostInfo rh $ Just state
|
||||||
Just rhi@RemoteHostInfo {remoteHostId} -> do
|
Just rhi@RemoteHostInfo {remoteHostId} -> do
|
||||||
withStore' $ \db -> updateHostPairing db remoteHostId hostDeviceName hostDhPubKey'
|
withStore' $ \db -> updateHostPairing db remoteHostId hostDeviceName hostDhPubKey' rcAddr_ port_
|
||||||
pure (rhi :: RemoteHostInfo) {sessionState = Just state}
|
pure (rhi :: RemoteHostInfo) {sessionState = Just state}
|
||||||
onDisconnected :: ChatMonad m => RHKey -> SessionSeq -> m ()
|
onDisconnected :: ChatMonad m => RHKey -> SessionSeq -> m ()
|
||||||
onDisconnected rhKey sseq = do
|
onDisconnected rhKey sseq = do
|
||||||
@@ -250,14 +256,15 @@ cancelRemoteHostSession :: ChatMonad m => Maybe (SessionSeq, RemoteHostStopReaso
|
|||||||
cancelRemoteHostSession handlerInfo_ rhKey = do
|
cancelRemoteHostSession handlerInfo_ rhKey = do
|
||||||
sessions <- asks remoteHostSessions
|
sessions <- asks remoteHostSessions
|
||||||
crh <- asks currentRemoteHost
|
crh <- asks currentRemoteHost
|
||||||
deregistered <- atomically $
|
deregistered <-
|
||||||
TM.lookup rhKey sessions >>= \case
|
atomically $
|
||||||
Nothing -> pure Nothing
|
TM.lookup rhKey sessions >>= \case
|
||||||
Just (sessSeq, _) | maybe False (/= sessSeq) (fst <$> handlerInfo_) -> pure Nothing -- ignore cancel from a ghost session handler
|
Nothing -> pure Nothing
|
||||||
Just (_, rhs) -> do
|
Just (sessSeq, _) | maybe False (/= sessSeq) (fst <$> handlerInfo_) -> pure Nothing -- ignore cancel from a ghost session handler
|
||||||
TM.delete rhKey sessions
|
Just (_, rhs) -> do
|
||||||
modifyTVar' crh $ \cur -> if (RHId <$> cur) == Just rhKey then Nothing else cur -- only wipe the closing RH
|
TM.delete rhKey sessions
|
||||||
pure $ Just rhs
|
modifyTVar' crh $ \cur -> if (RHId <$> cur) == Just rhKey then Nothing else cur -- only wipe the closing RH
|
||||||
|
pure $ Just rhs
|
||||||
forM_ deregistered $ \session -> do
|
forM_ deregistered $ \session -> do
|
||||||
liftIO $ cancelRemoteHost handlingError session `catchAny` (logError . tshow)
|
liftIO $ cancelRemoteHost handlingError session `catchAny` (logError . tshow)
|
||||||
forM_ (snd <$> handlerInfo_) $ \rhStopReason ->
|
forM_ (snd <$> handlerInfo_) $ \rhStopReason ->
|
||||||
@@ -312,8 +319,8 @@ switchRemoteHost rhId_ = do
|
|||||||
rhi_ <$ chatWriteVar currentRemoteHost rhId_
|
rhi_ <$ chatWriteVar currentRemoteHost rhId_
|
||||||
|
|
||||||
remoteHostInfo :: RemoteHost -> Maybe RemoteHostSessionState -> RemoteHostInfo
|
remoteHostInfo :: RemoteHost -> Maybe RemoteHostSessionState -> RemoteHostInfo
|
||||||
remoteHostInfo RemoteHost {remoteHostId, storePath, hostDeviceName} sessionState =
|
remoteHostInfo RemoteHost {remoteHostId, storePath, hostDeviceName, bindAddress_, bindPort_} sessionState =
|
||||||
RemoteHostInfo {remoteHostId, storePath, hostDeviceName, sessionState}
|
RemoteHostInfo {remoteHostId, storePath, hostDeviceName, bindAddress_, bindPort_, sessionState}
|
||||||
|
|
||||||
deleteRemoteHost :: ChatMonad m => RemoteHostId -> m ()
|
deleteRemoteHost :: ChatMonad m => RemoteHostId -> m ()
|
||||||
deleteRemoteHost rhId = do
|
deleteRemoteHost rhId = do
|
||||||
@@ -401,9 +408,10 @@ findKnownRemoteCtrl = do
|
|||||||
(RCCtrlPairing {ctrlFingerprint}, inv@(RCVerifiedInvitation RCInvitation {app})) <-
|
(RCCtrlPairing {ctrlFingerprint}, inv@(RCVerifiedInvitation RCInvitation {app})) <-
|
||||||
timeoutThrow (ChatErrorRemoteCtrl RCETimeout) discoveryTimeout . withAgent $ \a -> rcDiscoverCtrl a pairings
|
timeoutThrow (ChatErrorRemoteCtrl RCETimeout) discoveryTimeout . withAgent $ \a -> rcDiscoverCtrl a pairings
|
||||||
ctrlAppInfo_ <- (Just <$> parseCtrlAppInfo app) `catchChatError` const (pure Nothing)
|
ctrlAppInfo_ <- (Just <$> parseCtrlAppInfo app) `catchChatError` const (pure Nothing)
|
||||||
rc <- withStore' (`getRemoteCtrlByFingerprint` ctrlFingerprint) >>= \case
|
rc <-
|
||||||
Nothing -> throwChatError $ CEInternalError "connecting with a stored ctrl"
|
withStore' (`getRemoteCtrlByFingerprint` ctrlFingerprint) >>= \case
|
||||||
Just rc -> pure rc
|
Nothing -> throwChatError $ CEInternalError "connecting with a stored ctrl"
|
||||||
|
Just rc -> pure rc
|
||||||
atomically $ putTMVar foundCtrl (rc, inv)
|
atomically $ putTMVar foundCtrl (rc, inv)
|
||||||
let compatible = isJust $ compatibleAppVersion hostAppVersionRange . appVersionRange =<< ctrlAppInfo_
|
let compatible = isJust $ compatibleAppVersion hostAppVersionRange . appVersionRange =<< ctrlAppInfo_
|
||||||
toView CRRemoteCtrlFound {remoteCtrl = remoteCtrlInfo rc (Just RCSSearching), ctrlAppInfo_, appVersion = currentAppVersion, compatible}
|
toView CRRemoteCtrlFound {remoteCtrl = remoteCtrlInfo rc (Just RCSSearching), ctrlAppInfo_, appVersion = currentAppVersion, compatible}
|
||||||
@@ -422,7 +430,7 @@ confirmRemoteCtrl rcId = do
|
|||||||
pure $ Right (sseq, action, foundCtrl)
|
pure $ Right (sseq, action, foundCtrl)
|
||||||
_ -> pure . Left $ ChatErrorRemoteCtrl RCEBadState
|
_ -> pure . Left $ ChatErrorRemoteCtrl RCEBadState
|
||||||
uninterruptibleCancel listener
|
uninterruptibleCancel listener
|
||||||
(RemoteCtrl{remoteCtrlId = foundRcId}, verifiedInv) <- atomically $ takeTMVar found
|
(RemoteCtrl {remoteCtrlId = foundRcId}, verifiedInv) <- atomically $ takeTMVar found
|
||||||
unless (rcId == foundRcId) $ throwError $ ChatErrorRemoteCtrl RCEBadController
|
unless (rcId == foundRcId) $ throwError $ ChatErrorRemoteCtrl RCEBadController
|
||||||
connectRemoteCtrl verifiedInv sseq >>= \case
|
connectRemoteCtrl verifiedInv sseq >>= \case
|
||||||
(Nothing, _) -> throwChatError $ CEInternalError "connecting with a stored ctrl"
|
(Nothing, _) -> throwChatError $ CEInternalError "connecting with a stored ctrl"
|
||||||
@@ -647,10 +655,12 @@ handleCtrlError sseq mkReason name action =
|
|||||||
cancelActiveRemoteCtrl :: ChatMonad m => Maybe (SessionSeq, RemoteCtrlStopReason) -> m ()
|
cancelActiveRemoteCtrl :: ChatMonad m => Maybe (SessionSeq, RemoteCtrlStopReason) -> m ()
|
||||||
cancelActiveRemoteCtrl handlerInfo_ = handleAny (logError . tshow) $ do
|
cancelActiveRemoteCtrl handlerInfo_ = handleAny (logError . tshow) $ do
|
||||||
var <- asks remoteCtrlSession
|
var <- asks remoteCtrlSession
|
||||||
session_ <- atomically $ readTVar var >>= \case
|
session_ <-
|
||||||
Nothing -> pure Nothing
|
atomically $
|
||||||
Just (oldSeq, _) | maybe False (/= oldSeq) (fst <$> handlerInfo_) -> pure Nothing
|
readTVar var >>= \case
|
||||||
Just (_, s) -> Just s <$ writeTVar var Nothing
|
Nothing -> pure Nothing
|
||||||
|
Just (oldSeq, _) | (maybe False ((oldSeq /=) . fst) handlerInfo_) -> pure Nothing
|
||||||
|
Just (_, s) -> Just s <$ writeTVar var Nothing
|
||||||
forM_ session_ $ \session -> do
|
forM_ session_ $ \session -> do
|
||||||
liftIO $ cancelRemoteCtrl handlingError session
|
liftIO $ cancelRemoteCtrl handlingError session
|
||||||
forM_ (snd <$> handlerInfo_) $ \rcStopReason ->
|
forM_ (snd <$> handlerInfo_) $ \rcStopReason ->
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ module Simplex.Chat.Remote.AppVersion
|
|||||||
compatibleAppVersion,
|
compatibleAppVersion,
|
||||||
isAppCompatible,
|
isAppCompatible,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Data.Aeson (FromJSON (..), ToJSON (..))
|
import Data.Aeson (FromJSON (..), ToJSON (..))
|
||||||
import qualified Data.Aeson as J
|
import qualified Data.Aeson as J
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ import Network.Socket
|
|||||||
|
|
||||||
#include <HsNet.h>
|
#include <HsNet.h>
|
||||||
|
|
||||||
{- | Toggle multicast group membership.
|
-- | Toggle multicast group membership.
|
||||||
|
-- NB: Group membership is per-host, not per-process. A socket is only used to access system interface for groups.
|
||||||
NB: Group membership is per-host, not per-process. A socket is only used to access system interface for groups.
|
|
||||||
-}
|
|
||||||
setMembership :: Socket -> HostAddress -> Bool -> IO (Either CInt ())
|
setMembership :: Socket -> HostAddress -> Bool -> IO (Either CInt ())
|
||||||
setMembership sock group membership = allocaBytes #{size struct ip_mreq} $ \mReqPtr -> do
|
setMembership sock group membership = allocaBytes #{size struct ip_mreq} $ \mReqPtr -> do
|
||||||
#{poke struct ip_mreq, imr_multiaddr} mReqPtr group
|
#{poke struct ip_mreq, imr_multiaddr} mReqPtr group
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE PatternSynonyms #-}
|
{-# LANGUAGE PatternSynonyms #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
|
||||||
{-# LANGUAGE TupleSections #-}
|
{-# LANGUAGE TupleSections #-}
|
||||||
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
|
||||||
module Simplex.Chat.Remote.Protocol where
|
module Simplex.Chat.Remote.Protocol where
|
||||||
|
|
||||||
@@ -41,16 +41,16 @@ import Simplex.FileTransfer.Description (FileDigest (..))
|
|||||||
import Simplex.Messaging.Agent.Client (agentDRG)
|
import Simplex.Messaging.Agent.Client (agentDRG)
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
||||||
import Simplex.Messaging.Crypto.Lazy (LazyByteString)
|
import Simplex.Messaging.Crypto.Lazy (LazyByteString)
|
||||||
import Simplex.Messaging.Encoding
|
import Simplex.Messaging.Encoding
|
||||||
import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON, pattern SingleFieldJSONTag, pattern TaggedObjectJSONData, pattern TaggedObjectJSONTag)
|
import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON, pattern SingleFieldJSONTag, pattern TaggedObjectJSONData, pattern TaggedObjectJSONTag)
|
||||||
import Simplex.Messaging.Transport.Buffer (getBuffered)
|
import Simplex.Messaging.Transport.Buffer (getBuffered)
|
||||||
import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..), HTTP2BodyChunk, getBodyChunk)
|
import Simplex.Messaging.Transport.HTTP2 (HTTP2Body (..), HTTP2BodyChunk, getBodyChunk)
|
||||||
import Simplex.Messaging.Transport.HTTP2.Client (HTTP2Client, HTTP2Response (..), closeHTTP2Client, sendRequestDirect)
|
import Simplex.Messaging.Transport.HTTP2.Client (HTTP2Client, HTTP2Response (..), closeHTTP2Client, sendRequestDirect)
|
||||||
import Simplex.Messaging.Util (liftEitherError, liftEitherWith, liftError, tshow)
|
import Simplex.Messaging.Util (liftEitherError, liftEitherWith, liftError, tshow)
|
||||||
import Simplex.RemoteControl.Types (CtrlSessKeys (..), HostSessKeys (..), RCErrorType (..), SessionCode)
|
|
||||||
import Simplex.RemoteControl.Client (xrcpBlockSize)
|
import Simplex.RemoteControl.Client (xrcpBlockSize)
|
||||||
import qualified Simplex.RemoteControl.Client as RC
|
import qualified Simplex.RemoteControl.Client as RC
|
||||||
|
import Simplex.RemoteControl.Types (CtrlSessKeys (..), HostSessKeys (..), RCErrorType (..), SessionCode)
|
||||||
import System.FilePath (takeFileName, (</>))
|
import System.FilePath (takeFileName, (</>))
|
||||||
import UnliftIO
|
import UnliftIO
|
||||||
|
|
||||||
@@ -64,10 +64,10 @@ data RemoteCommand
|
|||||||
|
|
||||||
data RemoteResponse
|
data RemoteResponse
|
||||||
= RRChatResponse {chatResponse :: ChatResponse}
|
= RRChatResponse {chatResponse :: ChatResponse}
|
||||||
| RRChatEvent {chatEvent :: Maybe ChatResponse} -- ^ 'Nothing' on poll timeout
|
| RRChatEvent {chatEvent :: Maybe ChatResponse} -- 'Nothing' on poll timeout
|
||||||
| RRFileStored {filePath :: String}
|
| RRFileStored {filePath :: String}
|
||||||
| RRFile {fileSize :: Word32, fileDigest :: FileDigest} -- provides attachment , fileDigest :: FileDigest
|
| RRFile {fileSize :: Word32, fileDigest :: FileDigest} -- provides attachment , fileDigest :: FileDigest
|
||||||
| RRProtocolError {remoteProcotolError :: RemoteProtocolError} -- ^ The protocol error happened on the server side
|
| RRProtocolError {remoteProcotolError :: RemoteProtocolError} -- The protocol error happened on the server side
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
-- Force platform-independent encoding as the types aren't UI-visible
|
-- Force platform-independent encoding as the types aren't UI-visible
|
||||||
@@ -126,7 +126,7 @@ remoteStoreFile c localPath fileName = do
|
|||||||
r -> badResponse r
|
r -> badResponse r
|
||||||
|
|
||||||
remoteGetFile :: RemoteHostClient -> FilePath -> RemoteFile -> ExceptT RemoteProtocolError IO ()
|
remoteGetFile :: RemoteHostClient -> FilePath -> RemoteFile -> ExceptT RemoteProtocolError IO ()
|
||||||
remoteGetFile c@RemoteHostClient{encryption} destDir rf@RemoteFile {fileSource = CryptoFile {filePath}} =
|
remoteGetFile c@RemoteHostClient {encryption} destDir rf@RemoteFile {fileSource = CryptoFile {filePath}} =
|
||||||
sendRemoteCommand c Nothing RCGetFile {file = rf} >>= \case
|
sendRemoteCommand c Nothing RCGetFile {file = rf} >>= \case
|
||||||
(getChunk, RRFile {fileSize, fileDigest}) -> do
|
(getChunk, RRFile {fileSize, fileDigest}) -> do
|
||||||
-- TODO we could optimize by checking size and hash before receiving the file
|
-- TODO we could optimize by checking size and hash before receiving the file
|
||||||
@@ -140,7 +140,7 @@ sendRemoteCommand' c attachment_ rc = snd <$> sendRemoteCommand c attachment_ rc
|
|||||||
|
|
||||||
sendRemoteCommand :: RemoteHostClient -> Maybe (Handle, Word32) -> RemoteCommand -> ExceptT RemoteProtocolError IO (Int -> IO ByteString, RemoteResponse)
|
sendRemoteCommand :: RemoteHostClient -> Maybe (Handle, Word32) -> RemoteCommand -> ExceptT RemoteProtocolError IO (Int -> IO ByteString, RemoteResponse)
|
||||||
sendRemoteCommand RemoteHostClient {httpClient, hostEncoding, encryption} file_ cmd = do
|
sendRemoteCommand RemoteHostClient {httpClient, hostEncoding, encryption} file_ cmd = do
|
||||||
encFile_ <- mapM (prepareEncryptedFile encryption) file_
|
encFile_ <- mapM (prepareEncryptedFile encryption) file_
|
||||||
req <- httpRequest encFile_ <$> encryptEncodeHTTP2Body encryption (J.encode cmd)
|
req <- httpRequest encFile_ <$> encryptEncodeHTTP2Body encryption (J.encode cmd)
|
||||||
HTTP2Response {response, respBody} <- liftEitherError (RPEHTTP2 . tshow) $ sendRequestDirect httpClient req Nothing
|
HTTP2Response {response, respBody} <- liftEitherError (RPEHTTP2 . tshow) $ sendRequestDirect httpClient req Nothing
|
||||||
(header, getNext) <- parseDecryptHTTP2Body encryption response respBody
|
(header, getNext) <- parseDecryptHTTP2Body encryption response respBody
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ module Simplex.Chat.Remote.Transport where
|
|||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
import Data.ByteString.Builder (Builder, byteString)
|
|
||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
|
import Data.ByteString.Builder (Builder, byteString)
|
||||||
import qualified Data.ByteString.Lazy as LB
|
import qualified Data.ByteString.Lazy as LB
|
||||||
import Data.Word (Word32)
|
import Data.Word (Word32)
|
||||||
import Simplex.FileTransfer.Description (FileDigest (..))
|
|
||||||
import Simplex.Chat.Remote.Types
|
import Simplex.Chat.Remote.Types
|
||||||
|
import Simplex.FileTransfer.Description (FileDigest (..))
|
||||||
|
import Simplex.FileTransfer.Transport (ReceiveFileError (..), receiveSbFile, sendEncFile)
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||||
import Simplex.FileTransfer.Transport (ReceiveFileError (..), receiveSbFile, sendEncFile)
|
|
||||||
import Simplex.Messaging.Encoding
|
import Simplex.Messaging.Encoding
|
||||||
import Simplex.Messaging.Util (liftEitherError, liftEitherWith)
|
import Simplex.Messaging.Util (liftEitherError, liftEitherWith)
|
||||||
import Simplex.RemoteControl.Types (RCErrorType (..))
|
import Simplex.RemoteControl.Types (RCErrorType (..))
|
||||||
|
|||||||
@@ -18,16 +18,17 @@ import qualified Data.Aeson.TH as J
|
|||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
|
import Data.Word (Word16)
|
||||||
import Simplex.Chat.Remote.AppVersion
|
import Simplex.Chat.Remote.AppVersion
|
||||||
import Simplex.Chat.Types (verificationCode)
|
import Simplex.Chat.Types (verificationCode)
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
|
import Simplex.Messaging.Crypto.File (CryptoFile)
|
||||||
import Simplex.Messaging.Crypto.SNTRUP761 (KEMHybridSecret)
|
import Simplex.Messaging.Crypto.SNTRUP761 (KEMHybridSecret)
|
||||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON)
|
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON)
|
||||||
|
import Simplex.Messaging.Transport (TLS (..))
|
||||||
import Simplex.Messaging.Transport.HTTP2.Client (HTTP2Client)
|
import Simplex.Messaging.Transport.HTTP2.Client (HTTP2Client)
|
||||||
import Simplex.RemoteControl.Client
|
import Simplex.RemoteControl.Client
|
||||||
import Simplex.RemoteControl.Types
|
import Simplex.RemoteControl.Types
|
||||||
import Simplex.Messaging.Crypto.File (CryptoFile)
|
|
||||||
import Simplex.Messaging.Transport (TLS (..))
|
|
||||||
|
|
||||||
data RemoteHostClient = RemoteHostClient
|
data RemoteHostClient = RemoteHostClient
|
||||||
{ hostEncoding :: PlatformEncoding,
|
{ hostEncoding :: PlatformEncoding,
|
||||||
@@ -48,13 +49,13 @@ data RemoteCrypto = RemoteCrypto
|
|||||||
|
|
||||||
data RemoteSignatures
|
data RemoteSignatures
|
||||||
= RSSign
|
= RSSign
|
||||||
{ idPrivKey :: C.PrivateKeyEd25519,
|
{ idPrivKey :: C.PrivateKeyEd25519,
|
||||||
sessPrivKey :: C.PrivateKeyEd25519
|
sessPrivKey :: C.PrivateKeyEd25519
|
||||||
}
|
}
|
||||||
| RSVerify
|
| RSVerify
|
||||||
{ idPubKey :: C.PublicKeyEd25519,
|
{ idPubKey :: C.PublicKeyEd25519,
|
||||||
sessPubKey :: C.PublicKeyEd25519
|
sessPubKey :: C.PublicKeyEd25519
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionSeq = Int
|
type SessionSeq = Int
|
||||||
|
|
||||||
@@ -71,12 +72,12 @@ data RemoteHostSession
|
|||||||
| RHSessionPendingConfirmation {sessionCode :: Text, tls :: TLS, rhPendingSession :: RHPendingSession}
|
| RHSessionPendingConfirmation {sessionCode :: Text, tls :: TLS, rhPendingSession :: RHPendingSession}
|
||||||
| RHSessionConfirmed {tls :: TLS, rhPendingSession :: RHPendingSession}
|
| RHSessionConfirmed {tls :: TLS, rhPendingSession :: RHPendingSession}
|
||||||
| RHSessionConnected
|
| RHSessionConnected
|
||||||
{ rchClient :: RCHostClient,
|
{ rchClient :: RCHostClient,
|
||||||
tls :: TLS,
|
tls :: TLS,
|
||||||
rhClient :: RemoteHostClient,
|
rhClient :: RemoteHostClient,
|
||||||
pollAction :: Async (),
|
pollAction :: Async (),
|
||||||
storePath :: FilePath
|
storePath :: FilePath
|
||||||
}
|
}
|
||||||
|
|
||||||
data RemoteHostSessionState
|
data RemoteHostSessionState
|
||||||
= RHSStarting
|
= RHSStarting
|
||||||
@@ -128,6 +129,8 @@ data RemoteHost = RemoteHost
|
|||||||
{ remoteHostId :: RemoteHostId,
|
{ remoteHostId :: RemoteHostId,
|
||||||
hostDeviceName :: Text,
|
hostDeviceName :: Text,
|
||||||
storePath :: FilePath,
|
storePath :: FilePath,
|
||||||
|
bindAddress_ :: Maybe RCCtrlAddress,
|
||||||
|
bindPort_ :: Maybe Word16,
|
||||||
hostPairing :: RCHostPairing
|
hostPairing :: RCHostPairing
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +139,8 @@ data RemoteHostInfo = RemoteHostInfo
|
|||||||
{ remoteHostId :: RemoteHostId,
|
{ remoteHostId :: RemoteHostId,
|
||||||
hostDeviceName :: Text,
|
hostDeviceName :: Text,
|
||||||
storePath :: FilePath,
|
storePath :: FilePath,
|
||||||
|
bindAddress_ :: Maybe RCCtrlAddress,
|
||||||
|
bindPort_ :: Maybe Word16,
|
||||||
sessionState :: Maybe RemoteHostSessionState
|
sessionState :: Maybe RemoteHostSessionState
|
||||||
}
|
}
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
@@ -158,6 +163,7 @@ data PlatformEncoding
|
|||||||
deriving (Show, Eq)
|
deriving (Show, Eq)
|
||||||
|
|
||||||
localEncoding :: PlatformEncoding
|
localEncoding :: PlatformEncoding
|
||||||
|
|
||||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||||
localEncoding = PESwift
|
localEncoding = PESwift
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||||
|
|
||||||
module Simplex.Chat.Store.Connections
|
module Simplex.Chat.Store.Connections
|
||||||
@@ -25,11 +24,11 @@ import Data.Text (Text)
|
|||||||
import Data.Time.Clock (UTCTime (..))
|
import Data.Time.Clock (UTCTime (..))
|
||||||
import Database.SQLite.Simple (Only (..), (:.) (..))
|
import Database.SQLite.Simple (Only (..), (:.) (..))
|
||||||
import Database.SQLite.Simple.QQ (sql)
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
|
import Simplex.Chat.Protocol
|
||||||
import Simplex.Chat.Store.Files
|
import Simplex.Chat.Store.Files
|
||||||
import Simplex.Chat.Store.Groups
|
import Simplex.Chat.Store.Groups
|
||||||
import Simplex.Chat.Store.Profiles
|
import Simplex.Chat.Store.Profiles
|
||||||
import Simplex.Chat.Store.Shared
|
import Simplex.Chat.Store.Shared
|
||||||
import Simplex.Chat.Protocol
|
|
||||||
import Simplex.Chat.Types
|
import Simplex.Chat.Types
|
||||||
import Simplex.Chat.Types.Preferences
|
import Simplex.Chat.Types.Preferences
|
||||||
import Simplex.Messaging.Agent.Protocol (ConnId)
|
import Simplex.Messaging.Agent.Protocol (ConnId)
|
||||||
@@ -157,8 +156,9 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
|
|||||||
|
|
||||||
getConnectionEntityByConnReq :: DB.Connection -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity)
|
getConnectionEntityByConnReq :: DB.Connection -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity)
|
||||||
getConnectionEntityByConnReq db user@User {userId} (cReqSchema1, cReqSchema2) = do
|
getConnectionEntityByConnReq db user@User {userId} (cReqSchema1, cReqSchema2) = do
|
||||||
connId_ <- maybeFirstRow fromOnly $
|
connId_ <-
|
||||||
DB.query db "SELECT agent_conn_id FROM connections WHERE user_id = ? AND conn_req_inv IN (?,?) LIMIT 1" (userId, cReqSchema1, cReqSchema2)
|
maybeFirstRow fromOnly $
|
||||||
|
DB.query db "SELECT agent_conn_id FROM connections WHERE user_id = ? AND conn_req_inv IN (?,?) LIMIT 1" (userId, cReqSchema1, cReqSchema2)
|
||||||
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_
|
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_
|
||||||
|
|
||||||
-- search connection for connection plan:
|
-- search connection for connection plan:
|
||||||
@@ -167,21 +167,22 @@ getConnectionEntityByConnReq db user@User {userId} (cReqSchema1, cReqSchema2) =
|
|||||||
-- deleted connections are filtered out to allow re-connecting via same contact address
|
-- deleted connections are filtered out to allow re-connecting via same contact address
|
||||||
getContactConnEntityByConnReqHash :: DB.Connection -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity)
|
getContactConnEntityByConnReqHash :: DB.Connection -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity)
|
||||||
getContactConnEntityByConnReqHash db user@User {userId} (cReqHash1, cReqHash2) = do
|
getContactConnEntityByConnReqHash db user@User {userId} (cReqHash1, cReqHash2) = do
|
||||||
connId_ <- maybeFirstRow fromOnly $
|
connId_ <-
|
||||||
DB.query
|
maybeFirstRow fromOnly $
|
||||||
db
|
DB.query
|
||||||
[sql|
|
db
|
||||||
SELECT agent_conn_id FROM (
|
[sql|
|
||||||
SELECT
|
SELECT agent_conn_id FROM (
|
||||||
agent_conn_id,
|
SELECT
|
||||||
(CASE WHEN contact_id IS NOT NULL THEN 1 ELSE 0 END) AS conn_ord
|
agent_conn_id,
|
||||||
FROM connections
|
(CASE WHEN contact_id IS NOT NULL THEN 1 ELSE 0 END) AS conn_ord
|
||||||
WHERE user_id = ? AND via_contact_uri_hash IN (?,?) AND conn_status != ?
|
FROM connections
|
||||||
ORDER BY conn_ord DESC, created_at DESC
|
WHERE user_id = ? AND via_contact_uri_hash IN (?,?) AND conn_status != ?
|
||||||
LIMIT 1
|
ORDER BY conn_ord DESC, created_at DESC
|
||||||
)
|
LIMIT 1
|
||||||
|]
|
)
|
||||||
(userId, cReqHash1, cReqHash2, ConnDeleted)
|
|]
|
||||||
|
(userId, cReqHash1, cReqHash2, ConnDeleted)
|
||||||
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_
|
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db user) connId_
|
||||||
|
|
||||||
getConnectionsToSubscribe :: DB.Connection -> IO ([ConnId], [ConnectionEntity])
|
getConnectionsToSubscribe :: DB.Connection -> IO ([ConnId], [ConnectionEntity])
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
{-# LANGUAGE OverloadedRecordDot #-}
|
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
@@ -7,7 +6,6 @@
|
|||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE TupleSections #-}
|
{-# LANGUAGE TupleSections #-}
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||||
|
|
||||||
module Simplex.Chat.Store.Direct
|
module Simplex.Chat.Store.Direct
|
||||||
@@ -310,14 +308,14 @@ deleteUnusedProfile_ db userId profileId =
|
|||||||
updateContactProfile :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO Contact
|
updateContactProfile :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO Contact
|
||||||
updateContactProfile db user@User {userId} c p'
|
updateContactProfile db user@User {userId} c p'
|
||||||
| displayName == newName = do
|
| displayName == newName = do
|
||||||
liftIO $ updateContactProfile_ db userId profileId p'
|
liftIO $ updateContactProfile_ db userId profileId p'
|
||||||
pure c {profile, mergedPreferences}
|
pure c {profile, mergedPreferences}
|
||||||
| otherwise =
|
| otherwise =
|
||||||
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
|
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
updateContactProfile_' db userId profileId p' currentTs
|
updateContactProfile_' db userId profileId p' currentTs
|
||||||
updateContact_ db userId contactId localDisplayName ldn currentTs
|
updateContact_ db userId contactId localDisplayName ldn currentTs
|
||||||
pure $ Right c {localDisplayName = ldn, profile, mergedPreferences}
|
pure $ Right c {localDisplayName = ldn, profile, mergedPreferences}
|
||||||
where
|
where
|
||||||
Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}, userPreferences} = c
|
Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}, userPreferences} = c
|
||||||
Profile {displayName = newName, preferences} = p'
|
Profile {displayName = newName, preferences} = p'
|
||||||
@@ -481,7 +479,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
|
|||||||
ExceptT $
|
ExceptT $
|
||||||
maybeM getContactRequestByXContactId xContactId_ >>= \case
|
maybeM getContactRequestByXContactId xContactId_ >>= \case
|
||||||
Nothing -> createContactRequest
|
Nothing -> createContactRequest
|
||||||
Just cr -> updateContactRequest cr $> Right cr.contactRequestId
|
Just cr -> updateContactRequest cr $> Right (contactRequestId (cr :: UserContactRequest))
|
||||||
getContactRequest db user cReqId
|
getContactRequest db user cReqId
|
||||||
createContactRequest :: IO (Either StoreError Int64)
|
createContactRequest :: IO (Either StoreError Int64)
|
||||||
createContactRequest = do
|
createContactRequest = do
|
||||||
@@ -784,10 +782,8 @@ updateConnectionStatus :: DB.Connection -> Connection -> ConnStatus -> IO ()
|
|||||||
updateConnectionStatus db Connection {connId} connStatus = do
|
updateConnectionStatus db Connection {connId} connStatus = do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
if connStatus == ConnReady
|
if connStatus == ConnReady
|
||||||
then
|
then DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL WHERE connection_id = ?" (connStatus, currentTs, connId)
|
||||||
DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL WHERE connection_id = ?" (connStatus, currentTs, connId)
|
else DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId)
|
||||||
else
|
|
||||||
DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId)
|
|
||||||
|
|
||||||
updateContactSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO ()
|
updateContactSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO ()
|
||||||
updateContactSettings db User {userId} contactId ChatSettings {enableNtfs, sendRcpts, favorite} =
|
updateContactSettings db User {userId} contactId ChatSettings {enableNtfs, sendRcpts, favorite} =
|
||||||
@@ -816,4 +812,3 @@ resetContactConnInitiated db User {userId} Connection {connId} = do
|
|||||||
WHERE user_id = ? AND connection_id = ?
|
WHERE user_id = ? AND connection_id = ?
|
||||||
|]
|
|]
|
||||||
(updatedAt, userId, connId)
|
(updatedAt, userId, connId)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
{-# LANGUAGE GADTs #-}
|
{-# LANGUAGE GADTs #-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE OverloadedRecordDot #-}
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
@@ -109,7 +108,7 @@ import Simplex.Messaging.Protocol (SubscriptionMode (..))
|
|||||||
|
|
||||||
getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer]
|
getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer]
|
||||||
getLiveSndFileTransfers db User {userId} = do
|
getLiveSndFileTransfers db User {userId} = do
|
||||||
cutoffTs <- addUTCTime (- week) <$> getCurrentTime
|
cutoffTs <- addUTCTime (-week) <$> getCurrentTime
|
||||||
fileIds :: [Int64] <-
|
fileIds :: [Int64] <-
|
||||||
map fromOnly
|
map fromOnly
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
@@ -132,7 +131,7 @@ getLiveSndFileTransfers db User {userId} = do
|
|||||||
|
|
||||||
getLiveRcvFileTransfers :: DB.Connection -> User -> IO [RcvFileTransfer]
|
getLiveRcvFileTransfers :: DB.Connection -> User -> IO [RcvFileTransfer]
|
||||||
getLiveRcvFileTransfers db user@User {userId} = do
|
getLiveRcvFileTransfers db user@User {userId} = do
|
||||||
cutoffTs <- addUTCTime (- week) <$> getCurrentTime
|
cutoffTs <- addUTCTime (-week) <$> getCurrentTime
|
||||||
fileIds :: [Int64] <-
|
fileIds :: [Int64] <-
|
||||||
map fromOnly
|
map fromOnly
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
@@ -234,11 +233,12 @@ createSndGroupInlineFT db GroupMember {groupMemberId, localDisplayName = n} Conn
|
|||||||
|
|
||||||
updateSndDirectFTDelivery :: DB.Connection -> Contact -> FileTransferMeta -> Int64 -> ExceptT StoreError IO ()
|
updateSndDirectFTDelivery :: DB.Connection -> Contact -> FileTransferMeta -> Int64 -> ExceptT StoreError IO ()
|
||||||
updateSndDirectFTDelivery _ Contact {localDisplayName, activeConn = Nothing} _ _ = throwError $ SEContactNotReady localDisplayName
|
updateSndDirectFTDelivery _ Contact {localDisplayName, activeConn = Nothing} _ _ = throwError $ SEContactNotReady localDisplayName
|
||||||
updateSndDirectFTDelivery db Contact {activeConn = Just Connection {connId}} FileTransferMeta {fileId} msgDeliveryId = liftIO $
|
updateSndDirectFTDelivery db Contact {activeConn = Just Connection {connId}} FileTransferMeta {fileId} msgDeliveryId =
|
||||||
DB.execute
|
liftIO $
|
||||||
db
|
DB.execute
|
||||||
"UPDATE snd_files SET last_inline_msg_delivery_id = ? WHERE connection_id = ? AND file_id = ? AND file_inline IS NOT NULL"
|
db
|
||||||
(msgDeliveryId, connId, fileId)
|
"UPDATE snd_files SET last_inline_msg_delivery_id = ? WHERE connection_id = ? AND file_id = ? AND file_inline IS NOT NULL"
|
||||||
|
(msgDeliveryId, connId, fileId)
|
||||||
|
|
||||||
updateSndGroupFTDelivery :: DB.Connection -> GroupMember -> Connection -> FileTransferMeta -> Int64 -> IO ()
|
updateSndGroupFTDelivery :: DB.Connection -> GroupMember -> Connection -> FileTransferMeta -> Int64 -> IO ()
|
||||||
updateSndGroupFTDelivery db GroupMember {groupMemberId} Connection {connId} FileTransferMeta {fileId} msgDeliveryId =
|
updateSndGroupFTDelivery db GroupMember {groupMemberId} Connection {connId} FileTransferMeta {fileId} msgDeliveryId =
|
||||||
@@ -490,7 +490,7 @@ createRcvFileTransfer :: DB.Connection -> UserId -> Contact -> FileInvitation ->
|
|||||||
createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
|
createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
|
||||||
currentTs <- liftIO getCurrentTime
|
currentTs <- liftIO getCurrentTime
|
||||||
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
|
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
|
||||||
let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_
|
let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd_
|
||||||
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
|
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
|
||||||
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_
|
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_
|
||||||
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
|
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
|
||||||
@@ -511,7 +511,7 @@ createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupMember -> FileInvi
|
|||||||
createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
|
createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do
|
||||||
currentTs <- liftIO getCurrentTime
|
currentTs <- liftIO getCurrentTime
|
||||||
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
|
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
|
||||||
let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_
|
let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd_
|
||||||
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
|
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
|
||||||
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_
|
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_
|
||||||
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
|
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
|
||||||
@@ -724,7 +724,7 @@ removeFileCryptoArgs db fileId = do
|
|||||||
|
|
||||||
getRcvFilesToReceive :: DB.Connection -> User -> IO [RcvFileTransfer]
|
getRcvFilesToReceive :: DB.Connection -> User -> IO [RcvFileTransfer]
|
||||||
getRcvFilesToReceive db user@User {userId} = do
|
getRcvFilesToReceive db user@User {userId} = do
|
||||||
cutoffTs <- addUTCTime (- (2 * nominalDay)) <$> getCurrentTime
|
cutoffTs <- addUTCTime (-(2 * nominalDay)) <$> getCurrentTime
|
||||||
fileIds :: [Int64] <-
|
fileIds :: [Int64] <-
|
||||||
map fromOnly
|
map fromOnly
|
||||||
<$> DB.query
|
<$> DB.query
|
||||||
@@ -768,20 +768,20 @@ createRcvFileChunk db RcvFileTransfer {fileId, fileInvitation = FileInvitation {
|
|||||||
pure $ case map fromOnly ns of
|
pure $ case map fromOnly ns of
|
||||||
[]
|
[]
|
||||||
| chunkNo == 1 ->
|
| chunkNo == 1 ->
|
||||||
if chunkSize >= fileSize
|
if chunkSize >= fileSize
|
||||||
then RcvChunkFinal
|
then RcvChunkFinal
|
||||||
else RcvChunkOk
|
else RcvChunkOk
|
||||||
| otherwise -> RcvChunkError
|
| otherwise -> RcvChunkError
|
||||||
n : _
|
n : _
|
||||||
| chunkNo == n -> RcvChunkDuplicate
|
| chunkNo == n -> RcvChunkDuplicate
|
||||||
| chunkNo == n + 1 ->
|
| chunkNo == n + 1 ->
|
||||||
let prevSize = n * chunkSize
|
let prevSize = n * chunkSize
|
||||||
in if prevSize >= fileSize
|
in if prevSize >= fileSize
|
||||||
then RcvChunkError
|
then RcvChunkError
|
||||||
else
|
else
|
||||||
if prevSize + chunkSize >= fileSize
|
if prevSize + chunkSize >= fileSize
|
||||||
then RcvChunkFinal
|
then RcvChunkFinal
|
||||||
else RcvChunkOk
|
else RcvChunkOk
|
||||||
| otherwise -> RcvChunkError
|
| otherwise -> RcvChunkError
|
||||||
|
|
||||||
updatedRcvFileChunkStored :: DB.Connection -> RcvFileTransfer -> Integer -> IO ()
|
updatedRcvFileChunkStored :: DB.Connection -> RcvFileTransfer -> Integer -> IO ()
|
||||||
@@ -906,7 +906,7 @@ getLocalCryptoFile db userId fileId sent =
|
|||||||
_ -> do
|
_ -> do
|
||||||
unless sent $ throwError $ SEFileNotFound fileId
|
unless sent $ throwError $ SEFileNotFound fileId
|
||||||
FileTransferMeta {filePath, xftpSndFile} <- getFileTransferMeta_ db userId fileId
|
FileTransferMeta {filePath, xftpSndFile} <- getFileTransferMeta_ db userId fileId
|
||||||
pure $ CryptoFile filePath $ xftpSndFile >>= \f -> f.cryptoArgs
|
pure $ CryptoFile filePath $ xftpSndFile >>= \XFTPSndFile {cryptoArgs} -> cryptoArgs
|
||||||
|
|
||||||
updateDirectCIFileStatus :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> ExceptT StoreError IO AChatItem
|
updateDirectCIFileStatus :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> ExceptT StoreError IO AChatItem
|
||||||
updateDirectCIFileStatus db user fileId fileStatus = do
|
updateDirectCIFileStatus db user fileId fileStatus = do
|
||||||
|
|||||||
+142
-141
@@ -8,8 +8,6 @@
|
|||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE TupleSections #-}
|
{-# LANGUAGE TupleSections #-}
|
||||||
{-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE TypeOperators #-}
|
||||||
{-# LANGUAGE OverloadedRecordDot #-}
|
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||||
|
|
||||||
module Simplex.Chat.Store.Groups
|
module Simplex.Chat.Store.Groups
|
||||||
@@ -122,7 +120,7 @@ import Crypto.Random (ChaChaDRG)
|
|||||||
import Data.Either (rights)
|
import Data.Either (rights)
|
||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
import Data.List (partition, sortOn)
|
import Data.List (partition, sortOn)
|
||||||
import Data.Maybe (fromMaybe, isNothing, catMaybes, isJust)
|
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing)
|
||||||
import Data.Ord (Down (..))
|
import Data.Ord (Down (..))
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||||
@@ -359,7 +357,7 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo
|
|||||||
"INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?,?,?)"
|
"INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?,?,?)"
|
||||||
(profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs, currentTs)
|
(profileId, localDisplayName, connRequest, customUserProfileId, userId, True, currentTs, currentTs, currentTs)
|
||||||
insertedRowId db
|
insertedRowId db
|
||||||
let JVersionRange hostVRange = hostConn.peerChatVRange
|
let JVersionRange hostVRange = peerChatVRange hostConn
|
||||||
GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs hostVRange
|
GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing currentTs hostVRange
|
||||||
membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId currentTs supportedChatVRange
|
membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId currentTs supportedChatVRange
|
||||||
let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False}
|
let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False}
|
||||||
@@ -446,39 +444,39 @@ createGroupInvitedViaLink
|
|||||||
void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember GSMemAccepted IBUnknown customUserProfileId currentTs supportedChatVRange
|
void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember GSMemAccepted IBUnknown customUserProfileId currentTs supportedChatVRange
|
||||||
liftIO $ setViaGroupLinkHash db groupId connId
|
liftIO $ setViaGroupLinkHash db groupId connId
|
||||||
(,) <$> getGroupInfo db user groupId <*> getGroupMemberById db user hostMemberId
|
(,) <$> getGroupInfo db user groupId <*> getGroupMemberById db user hostMemberId
|
||||||
where
|
where
|
||||||
insertGroup_ currentTs = ExceptT $ do
|
insertGroup_ currentTs = ExceptT $ do
|
||||||
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
|
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
|
||||||
withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do
|
withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
|
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
|
||||||
(displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs)
|
(displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs)
|
||||||
profileId <- insertedRowId db
|
profileId <- insertedRowId db
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
"INSERT INTO groups (group_profile_id, local_display_name, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?,?)"
|
"INSERT INTO groups (group_profile_id, local_display_name, host_conn_custom_user_profile_id, user_id, enable_ntfs, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?,?)"
|
||||||
(profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs)
|
(profileId, localDisplayName, customUserProfileId, userId, True, currentTs, currentTs, currentTs)
|
||||||
insertedRowId db
|
insertedRowId db
|
||||||
insertHost_ currentTs groupId = ExceptT $ do
|
insertHost_ currentTs groupId = ExceptT $ do
|
||||||
let fromMemberProfile = profileFromName fromMemberName
|
let fromMemberProfile = profileFromName fromMemberName
|
||||||
withLocalDisplayName db userId fromMemberName $ \localDisplayName -> runExceptT $ do
|
withLocalDisplayName db userId fromMemberName $ \localDisplayName -> runExceptT $ do
|
||||||
(_, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs
|
(_, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs
|
||||||
let MemberIdRole {memberId, memberRole} = fromMember
|
let MemberIdRole {memberId, memberRole} = fromMember
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
INSERT INTO group_members
|
INSERT INTO group_members
|
||||||
( group_id, member_id, member_role, member_category, member_status, invited_by,
|
( group_id, member_id, member_role, member_category, member_status, invited_by,
|
||||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
|
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|]
|
|]
|
||||||
( (groupId, memberId, memberRole, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown)
|
( (groupId, memberId, memberRole, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown)
|
||||||
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
|
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
|
||||||
)
|
)
|
||||||
insertedRowId db
|
insertedRowId db
|
||||||
|
|
||||||
setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO ()
|
setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO ()
|
||||||
setViaGroupLinkHash db groupId connId =
|
setViaGroupLinkHash db groupId connId =
|
||||||
@@ -814,22 +812,22 @@ createAcceptedMember
|
|||||||
insertMember_ (MemberId memId) createdAt
|
insertMember_ (MemberId memId) createdAt
|
||||||
groupMemberId <- liftIO $ insertedRowId db
|
groupMemberId <- liftIO $ insertedRowId db
|
||||||
pure (groupMemberId, MemberId memId)
|
pure (groupMemberId, MemberId memId)
|
||||||
where
|
where
|
||||||
JVersionRange (VersionRange minV maxV) = cReqChatVRange
|
JVersionRange (VersionRange minV maxV) = cReqChatVRange
|
||||||
insertMember_ memberId createdAt =
|
insertMember_ memberId createdAt =
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
INSERT INTO group_members
|
INSERT INTO group_members
|
||||||
( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id,
|
||||||
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
|
user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at,
|
||||||
peer_chat_min_version, peer_chat_max_version)
|
peer_chat_min_version, peer_chat_max_version)
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|]
|
|]
|
||||||
( (groupId, memberId, memberRole, GCInviteeMember, GSMemAccepted, fromInvitedBy userContactId IBUser, groupMemberId' membership)
|
( (groupId, memberId, memberRole, GCInviteeMember, GSMemAccepted, fromInvitedBy userContactId IBUser, groupMemberId' membership)
|
||||||
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, createdAt, createdAt)
|
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, createdAt, createdAt)
|
||||||
:. (minV, maxV)
|
:. (minV, maxV)
|
||||||
)
|
)
|
||||||
|
|
||||||
createAcceptedMemberConnection :: DB.Connection -> User -> (CommandId, ConnId) -> UserContactRequest -> GroupMemberId -> SubscriptionMode -> IO ()
|
createAcceptedMemberConnection :: DB.Connection -> User -> (CommandId, ConnId) -> UserContactRequest -> GroupMemberId -> SubscriptionMode -> IO ()
|
||||||
createAcceptedMemberConnection
|
createAcceptedMemberConnection
|
||||||
@@ -957,23 +955,24 @@ createNewMember_
|
|||||||
:. (minV, maxV)
|
:. (minV, maxV)
|
||||||
)
|
)
|
||||||
groupMemberId <- insertedRowId db
|
groupMemberId <- insertedRowId db
|
||||||
pure GroupMember {
|
pure
|
||||||
groupMemberId,
|
GroupMember
|
||||||
groupId,
|
{ groupMemberId,
|
||||||
memberId,
|
groupId,
|
||||||
memberRole,
|
memberId,
|
||||||
memberCategory,
|
memberRole,
|
||||||
memberStatus,
|
memberCategory,
|
||||||
memberSettings = defaultMemberSettings,
|
memberStatus,
|
||||||
invitedBy,
|
memberSettings = defaultMemberSettings,
|
||||||
invitedByGroupMemberId = memInvitedByGroupMemberId,
|
invitedBy,
|
||||||
localDisplayName,
|
invitedByGroupMemberId = memInvitedByGroupMemberId,
|
||||||
memberProfile = toLocalProfile memberContactProfileId memberProfile "",
|
localDisplayName,
|
||||||
memberContactId,
|
memberProfile = toLocalProfile memberContactProfileId memberProfile "",
|
||||||
memberContactProfileId,
|
memberContactId,
|
||||||
activeConn,
|
memberContactProfileId,
|
||||||
memberChatVRange = JVersionRange mcvr
|
activeConn,
|
||||||
}
|
memberChatVRange = JVersionRange mcvr
|
||||||
|
}
|
||||||
|
|
||||||
checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId)
|
checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId)
|
||||||
checkGroupMemberHasItems db User {userId} GroupMember {groupMemberId, groupId} =
|
checkGroupMemberHasItems db User {userId} GroupMember {groupMemberId, groupId} =
|
||||||
@@ -1055,7 +1054,7 @@ saveIntroInvitation db reMember toMember introInv = do
|
|||||||
WHERE group_member_intro_id = :intro_id
|
WHERE group_member_intro_id = :intro_id
|
||||||
|]
|
|]
|
||||||
[ ":intro_status" := GMIntroInvReceived,
|
[ ":intro_status" := GMIntroInvReceived,
|
||||||
":group_queue_info" := introInv.groupConnReq,
|
":group_queue_info" := groupConnReq (introInv :: IntroInvitation),
|
||||||
":direct_queue_info" := directConnReq introInv,
|
":direct_queue_info" := directConnReq introInv,
|
||||||
":updated_at" := currentTs,
|
":updated_at" := currentTs,
|
||||||
":intro_id" := introId intro
|
":intro_id" := introId intro
|
||||||
@@ -1104,41 +1103,41 @@ getForwardIntroducedMembers :: DB.Connection -> User -> GroupMember -> Bool -> I
|
|||||||
getForwardIntroducedMembers db user invitee highlyAvailable = do
|
getForwardIntroducedMembers db user invitee highlyAvailable = do
|
||||||
memberIds <- map fromOnly <$> query
|
memberIds <- map fromOnly <$> query
|
||||||
filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db user) memberIds
|
filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db user) memberIds
|
||||||
where
|
where
|
||||||
mId = groupMemberId' invitee
|
mId = groupMemberId' invitee
|
||||||
query
|
query
|
||||||
| highlyAvailable = DB.query db q (mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected)
|
| highlyAvailable = DB.query db q (mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected)
|
||||||
| otherwise =
|
| otherwise =
|
||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
(q <> " AND intro_chat_protocol_version >= ?")
|
(q <> " AND intro_chat_protocol_version >= ?")
|
||||||
(mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, minVersion groupForwardVRange)
|
(mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, minVersion groupForwardVRange)
|
||||||
q =
|
q =
|
||||||
[sql|
|
[sql|
|
||||||
SELECT re_group_member_id
|
SELECT re_group_member_id
|
||||||
FROM group_member_intros
|
FROM group_member_intros
|
||||||
WHERE to_group_member_id = ? AND intro_status NOT IN (?,?,?)
|
WHERE to_group_member_id = ? AND intro_status NOT IN (?,?,?)
|
||||||
|]
|
|]
|
||||||
|
|
||||||
getForwardInvitedMembers :: DB.Connection -> User -> GroupMember -> Bool -> IO [GroupMember]
|
getForwardInvitedMembers :: DB.Connection -> User -> GroupMember -> Bool -> IO [GroupMember]
|
||||||
getForwardInvitedMembers db user forwardMember highlyAvailable = do
|
getForwardInvitedMembers db user forwardMember highlyAvailable = do
|
||||||
memberIds <- map fromOnly <$> query
|
memberIds <- map fromOnly <$> query
|
||||||
filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db user) memberIds
|
filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db user) memberIds
|
||||||
where
|
where
|
||||||
mId = groupMemberId' forwardMember
|
mId = groupMemberId' forwardMember
|
||||||
query
|
query
|
||||||
| highlyAvailable = DB.query db q (mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected)
|
| highlyAvailable = DB.query db q (mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected)
|
||||||
| otherwise =
|
| otherwise =
|
||||||
DB.query
|
DB.query
|
||||||
db
|
db
|
||||||
(q <> " AND intro_chat_protocol_version >= ?")
|
(q <> " AND intro_chat_protocol_version >= ?")
|
||||||
(mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, minVersion groupForwardVRange)
|
(mId, GMIntroReConnected, GMIntroToConnected, GMIntroConnected, minVersion groupForwardVRange)
|
||||||
q =
|
q =
|
||||||
[sql|
|
[sql|
|
||||||
SELECT to_group_member_id
|
SELECT to_group_member_id
|
||||||
FROM group_member_intros
|
FROM group_member_intros
|
||||||
WHERE re_group_member_id = ? AND intro_status NOT IN (?,?,?)
|
WHERE re_group_member_id = ? AND intro_status NOT IN (?,?,?)
|
||||||
|]
|
|]
|
||||||
|
|
||||||
createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> ExceptT StoreError IO GroupMember
|
createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> ExceptT StoreError IO GroupMember
|
||||||
createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memChatVRange memberProfile) (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do
|
createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memChatVRange memberProfile) (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do
|
||||||
@@ -1163,7 +1162,7 @@ createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupM
|
|||||||
|
|
||||||
createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionRange -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> IO ()
|
createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionRange -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> IO ()
|
||||||
createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do
|
createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do
|
||||||
let cLevel = 1 + maybe 0 (\Connection {connLevel} -> connLevel) activeConn
|
let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId mcvr viaContactId cLevel currentTs subMode
|
Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId mcvr viaContactId cLevel currentTs subMode
|
||||||
setCommandConnId db user groupCmdId groupConnId
|
setCommandConnId db user groupCmdId groupConnId
|
||||||
@@ -1263,15 +1262,15 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = do
|
|||||||
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
|
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
|
||||||
updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences}
|
updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences}
|
||||||
| displayName == newName = liftIO $ do
|
| displayName == newName = liftIO $ do
|
||||||
currentTs <- getCurrentTime
|
|
||||||
updateGroupProfile_ currentTs
|
|
||||||
pure (g :: GroupInfo) {groupProfile = p', fullGroupPreferences}
|
|
||||||
| otherwise =
|
|
||||||
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
|
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
updateGroupProfile_ currentTs
|
updateGroupProfile_ currentTs
|
||||||
updateGroup_ ldn currentTs
|
pure (g :: GroupInfo) {groupProfile = p', fullGroupPreferences}
|
||||||
pure $ Right (g :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences}
|
| otherwise =
|
||||||
|
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
|
||||||
|
currentTs <- getCurrentTime
|
||||||
|
updateGroupProfile_ currentTs
|
||||||
|
updateGroup_ ldn currentTs
|
||||||
|
pure $ Right (g :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences}
|
||||||
where
|
where
|
||||||
fullGroupPreferences = mergeGroupPreferences groupPreferences
|
fullGroupPreferences = mergeGroupPreferences groupPreferences
|
||||||
updateGroupProfile_ currentTs =
|
updateGroupProfile_ currentTs =
|
||||||
@@ -1317,31 +1316,33 @@ getGroupInfo db User {userId, userContactId} groupId =
|
|||||||
|
|
||||||
getGroupInfoByUserContactLinkConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo)
|
getGroupInfoByUserContactLinkConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo)
|
||||||
getGroupInfoByUserContactLinkConnReq db user@User {userId} (cReqSchema1, cReqSchema2) = do
|
getGroupInfoByUserContactLinkConnReq db user@User {userId} (cReqSchema1, cReqSchema2) = do
|
||||||
groupId_ <- maybeFirstRow fromOnly $
|
groupId_ <-
|
||||||
DB.query
|
maybeFirstRow fromOnly $
|
||||||
db
|
DB.query
|
||||||
[sql|
|
db
|
||||||
SELECT group_id
|
[sql|
|
||||||
FROM user_contact_links
|
SELECT group_id
|
||||||
WHERE user_id = ? AND conn_req_contact IN (?,?)
|
FROM user_contact_links
|
||||||
|]
|
WHERE user_id = ? AND conn_req_contact IN (?,?)
|
||||||
(userId, cReqSchema1, cReqSchema2)
|
|]
|
||||||
|
(userId, cReqSchema1, cReqSchema2)
|
||||||
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db user) groupId_
|
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db user) groupId_
|
||||||
|
|
||||||
getGroupInfoByGroupLinkHash :: DB.Connection -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo)
|
getGroupInfoByGroupLinkHash :: DB.Connection -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo)
|
||||||
getGroupInfoByGroupLinkHash db user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do
|
getGroupInfoByGroupLinkHash db user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do
|
||||||
groupId_ <- maybeFirstRow fromOnly $
|
groupId_ <-
|
||||||
DB.query
|
maybeFirstRow fromOnly $
|
||||||
db
|
DB.query
|
||||||
[sql|
|
db
|
||||||
SELECT g.group_id
|
[sql|
|
||||||
FROM groups g
|
SELECT g.group_id
|
||||||
JOIN group_members mu ON mu.group_id = g.group_id
|
FROM groups g
|
||||||
WHERE g.user_id = ? AND g.via_group_link_uri_hash IN (?,?)
|
JOIN group_members mu ON mu.group_id = g.group_id
|
||||||
AND mu.contact_id = ? AND mu.member_status NOT IN (?,?,?)
|
WHERE g.user_id = ? AND g.via_group_link_uri_hash IN (?,?)
|
||||||
LIMIT 1
|
AND mu.contact_id = ? AND mu.member_status NOT IN (?,?,?)
|
||||||
|]
|
LIMIT 1
|
||||||
(userId, groupLinkHash1, groupLinkHash2, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)
|
|]
|
||||||
|
(userId, groupLinkHash1, groupLinkHash2, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)
|
||||||
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db user) groupId_
|
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db user) groupId_
|
||||||
|
|
||||||
getGroupIdByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupId
|
getGroupIdByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupId
|
||||||
@@ -1935,18 +1936,18 @@ createMemberContactConn_
|
|||||||
updateMemberProfile :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember
|
updateMemberProfile :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember
|
||||||
updateMemberProfile db User {userId} m p'
|
updateMemberProfile db User {userId} m p'
|
||||||
| displayName == newName = do
|
| displayName == newName = do
|
||||||
liftIO $ updateContactProfile_ db userId profileId p'
|
liftIO $ updateContactProfile_ db userId profileId p'
|
||||||
pure m {memberProfile = profile}
|
pure m {memberProfile = profile}
|
||||||
| otherwise =
|
| otherwise =
|
||||||
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
|
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
updateContactProfile_' db userId profileId p' currentTs
|
updateContactProfile_' db userId profileId p' currentTs
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
"UPDATE group_members SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ?"
|
"UPDATE group_members SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ?"
|
||||||
(ldn, currentTs, userId, groupMemberId)
|
(ldn, currentTs, userId, groupMemberId)
|
||||||
DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId)
|
DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId)
|
||||||
pure $ Right m {localDisplayName = ldn, memberProfile = profile}
|
pure $ Right m {localDisplayName = ldn, memberProfile = profile}
|
||||||
where
|
where
|
||||||
GroupMember {groupMemberId, localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m
|
GroupMember {groupMemberId, localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m
|
||||||
Profile {displayName = newName} = p'
|
Profile {displayName = newName} = p'
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
{-# LANGUAGE TupleSections #-}
|
{-# LANGUAGE TupleSections #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
{-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||||
|
|
||||||
module Simplex.Chat.Store.Messages
|
module Simplex.Chat.Store.Messages
|
||||||
@@ -199,40 +198,41 @@ createNewMessageAndRcvMsgDelivery db connOrGroupId newMessage sharedMsgId_ RcvMs
|
|||||||
createMsgDeliveryEvent_ db msgDeliveryId MDSRcvAgent currentTs
|
createMsgDeliveryEvent_ db msgDeliveryId MDSRcvAgent currentTs
|
||||||
pure msg
|
pure msg
|
||||||
|
|
||||||
createNewRcvMessage :: forall e. (MsgEncodingI e) => DB.Connection -> ConnOrGroupId -> NewMessage e -> Maybe SharedMsgId -> Maybe GroupMemberId -> Maybe GroupMemberId -> ExceptT StoreError IO RcvMessage
|
createNewRcvMessage :: forall e. MsgEncodingI e => DB.Connection -> ConnOrGroupId -> NewMessage e -> Maybe SharedMsgId -> Maybe GroupMemberId -> Maybe GroupMemberId -> ExceptT StoreError IO RcvMessage
|
||||||
createNewRcvMessage db connOrGroupId NewMessage{chatMsgEvent, msgBody} sharedMsgId_ authorMember forwardedByMember =
|
createNewRcvMessage db connOrGroupId NewMessage {chatMsgEvent, msgBody} sharedMsgId_ authorMember forwardedByMember =
|
||||||
case connOrGroupId of
|
case connOrGroupId of
|
||||||
ConnectionId connId -> liftIO $ insertRcvMsg (Just connId) Nothing
|
ConnectionId connId -> liftIO $ insertRcvMsg (Just connId) Nothing
|
||||||
GroupId groupId -> case sharedMsgId_ of
|
GroupId groupId -> case sharedMsgId_ of
|
||||||
Just sharedMsgId -> liftIO (duplicateGroupMsgMemberIds groupId sharedMsgId) >>= \case
|
Just sharedMsgId ->
|
||||||
Just (duplAuthorId, duplFwdMemberId) ->
|
liftIO (duplicateGroupMsgMemberIds groupId sharedMsgId) >>= \case
|
||||||
throwError $ SEDuplicateGroupMessage groupId sharedMsgId duplAuthorId duplFwdMemberId
|
Just (duplAuthorId, duplFwdMemberId) ->
|
||||||
Nothing -> liftIO $ insertRcvMsg Nothing $ Just groupId
|
throwError $ SEDuplicateGroupMessage groupId sharedMsgId duplAuthorId duplFwdMemberId
|
||||||
|
Nothing -> liftIO $ insertRcvMsg Nothing $ Just groupId
|
||||||
Nothing -> liftIO $ insertRcvMsg Nothing $ Just groupId
|
Nothing -> liftIO $ insertRcvMsg Nothing $ Just groupId
|
||||||
where
|
where
|
||||||
duplicateGroupMsgMemberIds :: Int64 -> SharedMsgId -> IO (Maybe (Maybe GroupMemberId, Maybe GroupMemberId))
|
duplicateGroupMsgMemberIds :: Int64 -> SharedMsgId -> IO (Maybe (Maybe GroupMemberId, Maybe GroupMemberId))
|
||||||
duplicateGroupMsgMemberIds groupId sharedMsgId =
|
duplicateGroupMsgMemberIds groupId sharedMsgId =
|
||||||
maybeFirstRow id
|
maybeFirstRow id $
|
||||||
$ DB.query
|
DB.query
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
SELECT author_group_member_id, forwarded_by_group_member_id
|
||||||
|
FROM messages
|
||||||
|
WHERE group_id = ? AND shared_msg_id = ? LIMIT 1
|
||||||
|
|]
|
||||||
|
(groupId, sharedMsgId)
|
||||||
|
insertRcvMsg connId_ groupId_ = do
|
||||||
|
currentTs <- getCurrentTime
|
||||||
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT author_group_member_id, forwarded_by_group_member_id
|
INSERT INTO messages
|
||||||
FROM messages
|
(msg_sent, chat_msg_event, msg_body, created_at, updated_at, connection_id, group_id, shared_msg_id, author_group_member_id, forwarded_by_group_member_id)
|
||||||
WHERE group_id = ? AND shared_msg_id = ? LIMIT 1
|
VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||||
|]
|
|]
|
||||||
(groupId, sharedMsgId)
|
(MDRcv, toCMEventTag chatMsgEvent, msgBody, currentTs, currentTs, connId_, groupId_, sharedMsgId_, authorMember, forwardedByMember)
|
||||||
insertRcvMsg connId_ groupId_ = do
|
msgId <- insertedRowId db
|
||||||
currentTs <- getCurrentTime
|
pure RcvMessage {msgId, chatMsgEvent = ACME (encoding @e) chatMsgEvent, sharedMsgId_, msgBody, authorMember, forwardedByMember}
|
||||||
DB.execute
|
|
||||||
db
|
|
||||||
[sql|
|
|
||||||
INSERT INTO messages
|
|
||||||
(msg_sent, chat_msg_event, msg_body, created_at, updated_at, connection_id, group_id, shared_msg_id, author_group_member_id, forwarded_by_group_member_id)
|
|
||||||
VALUES (?,?,?,?,?,?,?,?,?,?)
|
|
||||||
|]
|
|
||||||
(MDRcv, toCMEventTag chatMsgEvent, msgBody, currentTs, currentTs, connId_, groupId_, sharedMsgId_, authorMember, forwardedByMember)
|
|
||||||
msgId <- insertedRowId db
|
|
||||||
pure RcvMessage{msgId, chatMsgEvent = ACME (encoding @e) chatMsgEvent, sharedMsgId_, msgBody, authorMember, forwardedByMember}
|
|
||||||
|
|
||||||
createSndMsgDeliveryEvent :: DB.Connection -> Int64 -> AgentMsgId -> MsgDeliveryStatus 'MDSnd -> ExceptT StoreError IO ()
|
createSndMsgDeliveryEvent :: DB.Connection -> Int64 -> AgentMsgId -> MsgDeliveryStatus 'MDSnd -> ExceptT StoreError IO ()
|
||||||
createSndMsgDeliveryEvent db connId agentMsgId sndMsgDeliveryStatus = do
|
createSndMsgDeliveryEvent db connId agentMsgId sndMsgDeliveryStatus = do
|
||||||
@@ -528,12 +528,12 @@ getDirectChatPreviews_ db user@User {userId} = do
|
|||||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||||
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT contact_id, MAX(chat_item_id) AS MaxId
|
SELECT contact_id, chat_item_id, MAX(created_at)
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
GROUP BY contact_id
|
GROUP BY contact_id
|
||||||
) MaxIds ON MaxIds.contact_id = ct.contact_id
|
) LastItems ON LastItems.contact_id = ct.contact_id
|
||||||
LEFT JOIN chat_items i ON i.contact_id = MaxIds.contact_id
|
LEFT JOIN chat_items i ON i.contact_id = LastItems.contact_id
|
||||||
AND i.chat_item_id = MaxIds.MaxId
|
AND i.chat_item_id = LastItems.chat_item_id
|
||||||
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
|
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread
|
SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread
|
||||||
@@ -615,12 +615,12 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
|
|||||||
JOIN group_members mu ON mu.group_id = g.group_id
|
JOIN group_members mu ON mu.group_id = g.group_id
|
||||||
JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id)
|
JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id)
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT group_id, MAX(chat_item_id) AS MaxId
|
SELECT group_id, chat_item_id, MAX(item_ts)
|
||||||
FROM chat_items
|
FROM chat_items
|
||||||
GROUP BY group_id
|
GROUP BY group_id
|
||||||
) MaxIds ON MaxIds.group_id = g.group_id
|
) LastItems ON LastItems.group_id = g.group_id
|
||||||
LEFT JOIN chat_items i ON i.group_id = MaxIds.group_id
|
LEFT JOIN chat_items i ON i.group_id = LastItems.group_id
|
||||||
AND i.chat_item_id = MaxIds.MaxId
|
AND i.chat_item_id = LastItems.chat_item_id
|
||||||
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
|
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread
|
SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread
|
||||||
@@ -724,7 +724,7 @@ getDirectChatItemsLast db User {userId} contactId count search = ExceptT $ do
|
|||||||
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
|
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
|
||||||
LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id
|
LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id
|
||||||
WHERE i.user_id = ? AND i.contact_id = ? AND i.item_text LIKE '%' || ? || '%'
|
WHERE i.user_id = ? AND i.contact_id = ? AND i.item_text LIKE '%' || ? || '%'
|
||||||
ORDER BY i.chat_item_id DESC
|
ORDER BY i.created_at DESC, i.chat_item_id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, contactId, search, count)
|
(userId, contactId, search, count)
|
||||||
@@ -754,7 +754,7 @@ getDirectChatAfter_ db User {userId} ct@Contact {contactId} afterChatItemId coun
|
|||||||
LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id
|
LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id
|
||||||
WHERE i.user_id = ? AND i.contact_id = ? AND i.item_text LIKE '%' || ? || '%'
|
WHERE i.user_id = ? AND i.contact_id = ? AND i.item_text LIKE '%' || ? || '%'
|
||||||
AND i.chat_item_id > ?
|
AND i.chat_item_id > ?
|
||||||
ORDER BY i.chat_item_id ASC
|
ORDER BY i.created_at ASC, i.chat_item_id ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, contactId, search, afterChatItemId, count)
|
(userId, contactId, search, afterChatItemId, count)
|
||||||
@@ -784,7 +784,7 @@ getDirectChatBefore_ db User {userId} ct@Contact {contactId} beforeChatItemId co
|
|||||||
LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id
|
LEFT JOIN chat_items ri ON ri.user_id = i.user_id AND ri.contact_id = i.contact_id AND ri.shared_msg_id = i.quoted_shared_msg_id
|
||||||
WHERE i.user_id = ? AND i.contact_id = ? AND i.item_text LIKE '%' || ? || '%'
|
WHERE i.user_id = ? AND i.contact_id = ? AND i.item_text LIKE '%' || ? || '%'
|
||||||
AND i.chat_item_id < ?
|
AND i.chat_item_id < ?
|
||||||
ORDER BY i.chat_item_id DESC
|
ORDER BY i.created_at DESC, i.chat_item_id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
|]
|
|]
|
||||||
(userId, contactId, search, beforeChatItemId, count)
|
(userId, contactId, search, beforeChatItemId, count)
|
||||||
@@ -1802,22 +1802,22 @@ getDirectReactions db ct itemSharedMId sent =
|
|||||||
setDirectReaction :: DB.Connection -> Contact -> SharedMsgId -> Bool -> MsgReaction -> Bool -> MessageId -> UTCTime -> IO ()
|
setDirectReaction :: DB.Connection -> Contact -> SharedMsgId -> Bool -> MsgReaction -> Bool -> MessageId -> UTCTime -> IO ()
|
||||||
setDirectReaction db ct itemSharedMId sent reaction add msgId reactionTs
|
setDirectReaction db ct itemSharedMId sent reaction add msgId reactionTs
|
||||||
| add =
|
| add =
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
INSERT INTO chat_item_reactions
|
INSERT INTO chat_item_reactions
|
||||||
(contact_id, shared_msg_id, reaction_sent, reaction, created_by_msg_id, reaction_ts)
|
(contact_id, shared_msg_id, reaction_sent, reaction, created_by_msg_id, reaction_ts)
|
||||||
VALUES (?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?)
|
||||||
|]
|
|]
|
||||||
(contactId' ct, itemSharedMId, sent, reaction, msgId, reactionTs)
|
(contactId' ct, itemSharedMId, sent, reaction, msgId, reactionTs)
|
||||||
| otherwise =
|
| otherwise =
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
DELETE FROM chat_item_reactions
|
DELETE FROM chat_item_reactions
|
||||||
WHERE contact_id = ? AND shared_msg_id = ? AND reaction_sent = ? AND reaction = ?
|
WHERE contact_id = ? AND shared_msg_id = ? AND reaction_sent = ? AND reaction = ?
|
||||||
|]
|
|]
|
||||||
(contactId' ct, itemSharedMId, sent, reaction)
|
(contactId' ct, itemSharedMId, sent, reaction)
|
||||||
|
|
||||||
getGroupReactions :: DB.Connection -> GroupInfo -> GroupMember -> MemberId -> SharedMsgId -> Bool -> IO [MsgReaction]
|
getGroupReactions :: DB.Connection -> GroupInfo -> GroupMember -> MemberId -> SharedMsgId -> Bool -> IO [MsgReaction]
|
||||||
getGroupReactions db GroupInfo {groupId} m itemMemberId itemSharedMId sent =
|
getGroupReactions db GroupInfo {groupId} m itemMemberId itemSharedMId sent =
|
||||||
@@ -1834,22 +1834,22 @@ getGroupReactions db GroupInfo {groupId} m itemMemberId itemSharedMId sent =
|
|||||||
setGroupReaction :: DB.Connection -> GroupInfo -> GroupMember -> MemberId -> SharedMsgId -> Bool -> MsgReaction -> Bool -> MessageId -> UTCTime -> IO ()
|
setGroupReaction :: DB.Connection -> GroupInfo -> GroupMember -> MemberId -> SharedMsgId -> Bool -> MsgReaction -> Bool -> MessageId -> UTCTime -> IO ()
|
||||||
setGroupReaction db GroupInfo {groupId} m itemMemberId itemSharedMId sent reaction add msgId reactionTs
|
setGroupReaction db GroupInfo {groupId} m itemMemberId itemSharedMId sent reaction add msgId reactionTs
|
||||||
| add =
|
| add =
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
INSERT INTO chat_item_reactions
|
INSERT INTO chat_item_reactions
|
||||||
(group_id, group_member_id, item_member_id, shared_msg_id, reaction_sent, reaction, created_by_msg_id, reaction_ts)
|
(group_id, group_member_id, item_member_id, shared_msg_id, reaction_sent, reaction, created_by_msg_id, reaction_ts)
|
||||||
VALUES (?,?,?,?,?,?,?,?)
|
VALUES (?,?,?,?,?,?,?,?)
|
||||||
|]
|
|]
|
||||||
(groupId, groupMemberId' m, itemMemberId, itemSharedMId, sent, reaction, msgId, reactionTs)
|
(groupId, groupMemberId' m, itemMemberId, itemSharedMId, sent, reaction, msgId, reactionTs)
|
||||||
| otherwise =
|
| otherwise =
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
DELETE FROM chat_item_reactions
|
DELETE FROM chat_item_reactions
|
||||||
WHERE group_id = ? AND group_member_id = ? AND shared_msg_id = ? AND item_member_id = ? AND reaction_sent = ? AND reaction = ?
|
WHERE group_id = ? AND group_member_id = ? AND shared_msg_id = ? AND item_member_id = ? AND reaction_sent = ? AND reaction = ?
|
||||||
|]
|
|]
|
||||||
(groupId, groupMemberId' m, itemSharedMId, itemMemberId, sent, reaction)
|
(groupId, groupMemberId' m, itemSharedMId, itemMemberId, sent, reaction)
|
||||||
|
|
||||||
getTimedItems :: DB.Connection -> User -> UTCTime -> IO [((ChatRef, ChatItemId), UTCTime)]
|
getTimedItems :: DB.Connection -> User -> UTCTime -> IO [((ChatRef, ChatItemId), UTCTime)]
|
||||||
getTimedItems db User {userId} startTimedThreadCutoff =
|
getTimedItems db User {userId} startTimedThreadCutoff =
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ import Simplex.Chat.Migrations.M20231030_xgrplinkmem_received
|
|||||||
import Simplex.Chat.Migrations.M20231107_indexes
|
import Simplex.Chat.Migrations.M20231107_indexes
|
||||||
import Simplex.Chat.Migrations.M20231113_group_forward
|
import Simplex.Chat.Migrations.M20231113_group_forward
|
||||||
import Simplex.Chat.Migrations.M20231114_remote_control
|
import Simplex.Chat.Migrations.M20231114_remote_control
|
||||||
|
import Simplex.Chat.Migrations.M20231126_remote_ctrl_address
|
||||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||||
|
|
||||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||||
@@ -179,7 +180,8 @@ schemaMigrations =
|
|||||||
("20231030_xgrplinkmem_received", m20231030_xgrplinkmem_received, Just down_m20231030_xgrplinkmem_received),
|
("20231030_xgrplinkmem_received", m20231030_xgrplinkmem_received, Just down_m20231030_xgrplinkmem_received),
|
||||||
("20231107_indexes", m20231107_indexes, Just down_m20231107_indexes),
|
("20231107_indexes", m20231107_indexes, Just down_m20231107_indexes),
|
||||||
("20231113_group_forward", m20231113_group_forward, Just down_m20231113_group_forward),
|
("20231113_group_forward", m20231113_group_forward, Just down_m20231113_group_forward),
|
||||||
("20231114_remote_control", m20231114_remote_control, Just down_m20231114_remote_control)
|
("20231114_remote_control", m20231114_remote_control, Just down_m20231114_remote_control),
|
||||||
|
("20231126_remote_ctrl_address", m20231126_remote_ctrl_address, Just down_m20231126_remote_ctrl_address)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | The list of migrations in ascending order by date
|
-- | The list of migrations in ascending order by date
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
{-# LANGUAGE TupleSections #-}
|
{-# LANGUAGE TupleSections #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
{-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
|
||||||
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
||||||
|
|
||||||
module Simplex.Chat.Store.Profiles
|
module Simplex.Chat.Store.Profiles
|
||||||
@@ -66,9 +65,9 @@ import Control.Monad.IO.Class
|
|||||||
import qualified Data.Aeson.TH as J
|
import qualified Data.Aeson.TH as J
|
||||||
import Data.Functor (($>))
|
import Data.Functor (($>))
|
||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
|
import Data.List.NonEmpty (NonEmpty)
|
||||||
import qualified Data.List.NonEmpty as L
|
import qualified Data.List.NonEmpty as L
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.List.NonEmpty (NonEmpty)
|
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||||
@@ -89,7 +88,7 @@ import Simplex.Messaging.Encoding.String
|
|||||||
import Simplex.Messaging.Parsers (defaultJSON)
|
import Simplex.Messaging.Parsers (defaultJSON)
|
||||||
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode)
|
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode)
|
||||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||||
import Simplex.Messaging.Util (safeDecodeUtf8, eitherToMaybe)
|
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8)
|
||||||
|
|
||||||
createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User
|
createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User
|
||||||
createUserRecord db auId p activeUser = createUserRecordAt db auId p activeUser =<< liftIO getCurrentTime
|
createUserRecord db auId p activeUser = createUserRecordAt db auId p activeUser =<< liftIO getCurrentTime
|
||||||
@@ -248,19 +247,19 @@ updateUserGroupReceipts db User {userId} UserMsgReceiptSettings {enable, clearOv
|
|||||||
updateUserProfile :: DB.Connection -> User -> Profile -> ExceptT StoreError IO User
|
updateUserProfile :: DB.Connection -> User -> Profile -> ExceptT StoreError IO User
|
||||||
updateUserProfile db user p'
|
updateUserProfile db user p'
|
||||||
| displayName == newName = do
|
| displayName == newName = do
|
||||||
liftIO $ updateContactProfile_ db userId profileId p'
|
liftIO $ updateContactProfile_ db userId profileId p'
|
||||||
pure user {profile, fullPreferences}
|
pure user {profile, fullPreferences}
|
||||||
| otherwise =
|
| otherwise =
|
||||||
checkConstraint SEDuplicateName . liftIO $ do
|
checkConstraint SEDuplicateName . liftIO $ do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
DB.execute db "UPDATE users SET local_display_name = ?, updated_at = ? WHERE user_id = ?" (newName, currentTs, userId)
|
DB.execute db "UPDATE users SET local_display_name = ?, updated_at = ? WHERE user_id = ?" (newName, currentTs, userId)
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
"INSERT INTO display_names (local_display_name, ldn_base, user_id, created_at, updated_at) VALUES (?,?,?,?,?)"
|
"INSERT INTO display_names (local_display_name, ldn_base, user_id, created_at, updated_at) VALUES (?,?,?,?,?)"
|
||||||
(newName, newName, userId, currentTs, currentTs)
|
(newName, newName, userId, currentTs, currentTs)
|
||||||
updateContactProfile_' db userId profileId p' currentTs
|
updateContactProfile_' db userId profileId p' currentTs
|
||||||
updateContact_ db userId userContactId localDisplayName newName currentTs
|
updateContact_ db userId userContactId localDisplayName newName currentTs
|
||||||
pure user {localDisplayName = newName, profile, fullPreferences}
|
pure user {localDisplayName = newName, profile, fullPreferences}
|
||||||
where
|
where
|
||||||
User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}} = user
|
User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias}} = user
|
||||||
Profile {displayName = newName, preferences} = p'
|
Profile {displayName = newName, preferences} = p'
|
||||||
@@ -297,7 +296,7 @@ getUserContactProfiles db User {userId} =
|
|||||||
|]
|
|]
|
||||||
(Only userId)
|
(Only userId)
|
||||||
where
|
where
|
||||||
toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) -> Profile
|
toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) -> (Profile)
|
||||||
toContactProfile (displayName, fullName, image, contactLink, preferences) = Profile {displayName, fullName, image, contactLink, preferences}
|
toContactProfile (displayName, fullName, image, contactLink, preferences) = Profile {displayName, fullName, image, contactLink, preferences}
|
||||||
|
|
||||||
createUserContactLink :: DB.Connection -> User -> ConnId -> ConnReqContact -> SubscriptionMode -> ExceptT StoreError IO ()
|
createUserContactLink :: DB.Connection -> User -> ConnId -> ConnReqContact -> SubscriptionMode -> ExceptT StoreError IO ()
|
||||||
@@ -457,17 +456,18 @@ getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) =
|
|||||||
|
|
||||||
getContactWithoutConnViaAddress :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact)
|
getContactWithoutConnViaAddress :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact)
|
||||||
getContactWithoutConnViaAddress db user@User {userId} (cReqSchema1, cReqSchema2) = do
|
getContactWithoutConnViaAddress db user@User {userId} (cReqSchema1, cReqSchema2) = do
|
||||||
ctId_ <- maybeFirstRow fromOnly $
|
ctId_ <-
|
||||||
DB.query
|
maybeFirstRow fromOnly $
|
||||||
db
|
DB.query
|
||||||
[sql|
|
db
|
||||||
SELECT ct.contact_id
|
[sql|
|
||||||
FROM contacts ct
|
SELECT ct.contact_id
|
||||||
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
|
FROM contacts ct
|
||||||
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
|
||||||
WHERE cp.user_id = ? AND cp.contact_link IN (?,?) AND c.connection_id IS NULL
|
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
||||||
|]
|
WHERE cp.user_id = ? AND cp.contact_link IN (?,?) AND c.connection_id IS NULL
|
||||||
(userId, cReqSchema1, cReqSchema2)
|
|]
|
||||||
|
(userId, cReqSchema1, cReqSchema2)
|
||||||
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db user) ctId_
|
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db user) ctId_
|
||||||
|
|
||||||
updateUserAddressAutoAccept :: DB.Connection -> User -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink
|
updateUserAddressAutoAccept :: DB.Connection -> User -> Maybe AutoAccept -> ExceptT StoreError IO UserContactLink
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user