Merge branch 'master' into master-ghc8107
This commit is contained in:
commit
ed6b3bbead
43
.github/workflows/build.yml
vendored
43
.github/workflows/build.yml
vendored
@ -263,11 +263,38 @@ jobs:
|
||||
# * 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'
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: ucrt64
|
||||
update: true
|
||||
install: >-
|
||||
git
|
||||
perl
|
||||
make
|
||||
pacboy: >-
|
||||
toolchain:p
|
||||
cmake:p
|
||||
|
||||
|
||||
- name: Windows build
|
||||
id: windows_build
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: cmd
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
export PATH=$PATH:/c/ghcup/bin
|
||||
scripts/desktop/prepare-openssl-windows.sh
|
||||
openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-1.1.1w | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g')
|
||||
rm cabal.project.local 2>/dev/null || true
|
||||
echo "ignore-project: False" >> cabal.project.local
|
||||
echo "package direct-sqlcipher" >> cabal.project.local
|
||||
echo " flags: +openssl" >> cabal.project.local
|
||||
echo " extra-include-dirs: $openssl_windows_style_path\include" >> cabal.project.local
|
||||
echo " extra-lib-dirs: $openssl_windows_style_path" >> cabal.project.local
|
||||
|
||||
rm -rf dist-newstyle/src/direct-sq*
|
||||
sed -i "s/, unix /--, unix /" simplex-chat.cabal
|
||||
cabal build --enable-tests
|
||||
rm -rf dist-newstyle/src/direct-sq*
|
||||
path=$(cabal list-bin simplex-chat | tail -n 1)
|
||||
@ -293,20 +320,6 @@ jobs:
|
||||
body: |
|
||||
${{ steps.windows_build.outputs.bin_hash }}
|
||||
|
||||
- name: 'Setup MSYS2'
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: ucrt64
|
||||
update: true
|
||||
install: >-
|
||||
git
|
||||
perl
|
||||
make
|
||||
pacboy: >-
|
||||
toolchain:p
|
||||
cmake:p
|
||||
|
||||
- name: Windows build desktop
|
||||
id: windows_desktop_build
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest'
|
||||
|
@ -85,6 +85,8 @@ final class ChatModel: ObservableObject {
|
||||
@Published var activeCall: Call?
|
||||
@Published var callCommand: WCallCommand?
|
||||
@Published var showCallView = false
|
||||
// remote desktop
|
||||
@Published var remoteCtrlSession: RemoteCtrlSession?
|
||||
// currently showing QR code
|
||||
@Published var connReqInv: String?
|
||||
// audio recording and playback
|
||||
@ -110,6 +112,10 @@ final class ChatModel: ObservableObject {
|
||||
notificationMode == .periodic || ntfEnablePeriodicGroupDefault.get()
|
||||
}
|
||||
|
||||
var activeRemoteCtrl: Bool {
|
||||
remoteCtrlSession?.active ?? false
|
||||
}
|
||||
|
||||
func getUser(_ userId: Int64) -> User? {
|
||||
currentUser?.userId == userId
|
||||
? currentUser
|
||||
@ -762,3 +768,32 @@ final class GMember: ObservableObject, Identifiable {
|
||||
var viewId: String { get { "\(wrapped.id) \(created.timeIntervalSince1970)" } }
|
||||
static let sampleData = GMember(GroupMember.sampleData)
|
||||
}
|
||||
|
||||
struct RemoteCtrlSession {
|
||||
var ctrlAppInfo: CtrlAppInfo
|
||||
var appVersion: String
|
||||
var sessionState: UIRemoteCtrlSessionState
|
||||
|
||||
func updateState(_ state: UIRemoteCtrlSessionState) -> RemoteCtrlSession {
|
||||
RemoteCtrlSession(ctrlAppInfo: ctrlAppInfo, appVersion: appVersion, sessionState: state)
|
||||
}
|
||||
|
||||
var active: Bool {
|
||||
if case .connected = sessionState { true } else { false }
|
||||
}
|
||||
|
||||
var sessionCode: String? {
|
||||
switch sessionState {
|
||||
case let .pendingConfirmation(_, sessionCode): sessionCode
|
||||
case let .connected(_, sessionCode): sessionCode
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum UIRemoteCtrlSessionState {
|
||||
case starting
|
||||
case connecting(remoteCtrl_: RemoteCtrlInfo?)
|
||||
case pendingConfirmation(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String)
|
||||
case connected(remoteCtrl: RemoteCtrlInfo, sessionCode: String)
|
||||
}
|
||||
|
@ -905,6 +905,44 @@ func apiCancelFile(fileId: Int64) async -> AChatItem? {
|
||||
}
|
||||
}
|
||||
|
||||
func setLocalDeviceName(_ displayName: String) throws {
|
||||
try sendCommandOkRespSync(.setLocalDeviceName(displayName: displayName))
|
||||
}
|
||||
|
||||
func connectRemoteCtrl(desktopAddress: String) async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String) {
|
||||
let r = await chatSendCmd(.connectRemoteCtrl(xrcpInvitation: desktopAddress))
|
||||
if case let .remoteCtrlConnecting(rc_, ctrlAppInfo, v) = r { return (rc_, ctrlAppInfo, v) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func findKnownRemoteCtrl() async throws {
|
||||
try await sendCommandOkResp(.findKnownRemoteCtrl)
|
||||
}
|
||||
|
||||
func confirmRemoteCtrl(_ rcId: Int64) async throws {
|
||||
try await sendCommandOkResp(.confirmRemoteCtrl(remoteCtrlId: rcId))
|
||||
}
|
||||
|
||||
func verifyRemoteCtrlSession(_ sessCode: String) async throws -> RemoteCtrlInfo {
|
||||
let r = await chatSendCmd(.verifyRemoteCtrlSession(sessionCode: sessCode))
|
||||
if case let .remoteCtrlConnected(rc) = r { return rc }
|
||||
throw r
|
||||
}
|
||||
|
||||
func listRemoteCtrls() throws -> [RemoteCtrlInfo] {
|
||||
let r = chatSendCmdSync(.listRemoteCtrls)
|
||||
if case let .remoteCtrlList(rcInfo) = r { return rcInfo }
|
||||
throw r
|
||||
}
|
||||
|
||||
func stopRemoteCtrl() async throws {
|
||||
try await sendCommandOkResp(.stopRemoteCtrl)
|
||||
}
|
||||
|
||||
func deleteRemoteCtrl(_ rcId: Int64) async throws {
|
||||
try await sendCommandOkResp(.deleteRemoteCtrl(remoteCtrlId: rcId))
|
||||
}
|
||||
|
||||
func networkErrorAlert(_ r: ChatResponse) -> Alert? {
|
||||
switch r {
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
|
||||
@ -1033,6 +1071,12 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
private func sendCommandOkRespSync(_ cmd: ChatCommand) throws {
|
||||
let r = chatSendCmdSync(cmd)
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiNewGroup(incognito: Bool, groupProfile: GroupProfile) throws -> GroupInfo {
|
||||
let userId = try currentUserId("apiNewGroup")
|
||||
let r = chatSendCmdSync(.apiNewGroup(userId: userId, incognito: incognito, groupProfile: groupProfile))
|
||||
@ -1670,6 +1714,26 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
await MainActor.run {
|
||||
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
|
||||
}
|
||||
case let .remoteCtrlFound(remoteCtrl):
|
||||
// TODO multicast
|
||||
logger.debug("\(String(describing: remoteCtrl))")
|
||||
case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode):
|
||||
await MainActor.run {
|
||||
let state = UIRemoteCtrlSessionState.pendingConfirmation(remoteCtrl_: remoteCtrl_, sessionCode: sessionCode)
|
||||
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state)
|
||||
}
|
||||
case let .remoteCtrlConnected(remoteCtrl):
|
||||
// TODO currently it is returned in response to command, so it is redundant
|
||||
await MainActor.run {
|
||||
let state = UIRemoteCtrlSessionState.connected(remoteCtrl: remoteCtrl, sessionCode: m.remoteCtrlSession?.sessionCode ?? "")
|
||||
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state)
|
||||
}
|
||||
case .remoteCtrlStopped:
|
||||
// This delay is needed to cancel the session that fails on network failure,
|
||||
// e.g. when user did not grant permission to access local network yet.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
switchToLocalSession()
|
||||
}
|
||||
default:
|
||||
logger.debug("unsupported event: \(res.responseType)")
|
||||
}
|
||||
@ -1683,6 +1747,19 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
}
|
||||
|
||||
func switchToLocalSession() {
|
||||
let m = ChatModel.shared
|
||||
m.remoteCtrlSession = nil
|
||||
do {
|
||||
m.users = try listUsers()
|
||||
try getUserChatData()
|
||||
let statuses = (try apiGetNetworkStatuses()).map { s in (s.agentConnId, s.networkStatus) }
|
||||
m.networkStatuses = Dictionary(uniqueKeysWithValues: statuses)
|
||||
} catch let error {
|
||||
logger.debug("error updating chat data: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
func active(_ user: any UserLike) -> Bool {
|
||||
user.userId == ChatModel.shared.currentUser?.id
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ struct ChatListView: View {
|
||||
@State private var searchText = ""
|
||||
@State private var showAddChat = false
|
||||
@State private var userPickerVisible = false
|
||||
@State private var showConnectDesktop = false
|
||||
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||
|
||||
var body: some View {
|
||||
@ -48,7 +49,14 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
UserPicker(showSettings: $showSettings, userPickerVisible: $userPickerVisible)
|
||||
UserPicker(
|
||||
showSettings: $showSettings,
|
||||
showConnectDesktop: $showConnectDesktop,
|
||||
userPickerVisible: $userPickerVisible
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $showConnectDesktop) {
|
||||
ConnectDesktopView()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ struct UserPicker: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var showConnectDesktop: Bool
|
||||
@Binding var userPickerVisible: Bool
|
||||
@State var scrollViewContentSize: CGSize = .zero
|
||||
@State var disableScrolling: Bool = true
|
||||
@ -62,6 +63,13 @@ struct UserPicker: View {
|
||||
.simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000))
|
||||
.frame(maxHeight: scrollViewContentSize.height)
|
||||
|
||||
menuButton("Use from desktop", icon: "desktopcomputer") {
|
||||
showConnectDesktop = true
|
||||
withAnimation {
|
||||
userPickerVisible.toggle()
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
menuButton("Settings", icon: "gearshape") {
|
||||
showSettings = true
|
||||
withAnimation {
|
||||
@ -85,7 +93,7 @@ struct UserPicker: View {
|
||||
do {
|
||||
m.users = try listUsers()
|
||||
} catch let error {
|
||||
logger.error("Error updating users \(responseError(error))")
|
||||
logger.error("Error loading users \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -144,7 +152,8 @@ struct UserPicker: View {
|
||||
.overlay(DetermineWidth())
|
||||
Spacer()
|
||||
Image(systemName: icon)
|
||||
// .frame(width: 24, alignment: .center)
|
||||
.symbolRenderingMode(.monochrome)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 22)
|
||||
@ -170,6 +179,7 @@ struct UserPicker_Previews: PreviewProvider {
|
||||
m.users = [UserInfo.sampleData, UserInfo.sampleData]
|
||||
return UserPicker(
|
||||
showSettings: Binding.constant(false),
|
||||
showConnectDesktop: Binding.constant(false),
|
||||
userPickerVisible: Binding.constant(true)
|
||||
)
|
||||
.environmentObject(m)
|
||||
|
434
apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift
Normal file
434
apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift
Normal file
@ -0,0 +1,434 @@
|
||||
//
|
||||
// ConnectDesktopView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 13/10/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import CodeScanner
|
||||
|
||||
struct ConnectDesktopView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
var viaSettings = false
|
||||
@AppStorage(DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS) private var deviceName = UIDevice.current.name
|
||||
@AppStorage(DEFAULT_CONFIRM_REMOTE_SESSIONS) private var confirmRemoteSessions = false
|
||||
@AppStorage(DEFAULT_CONNECT_REMOTE_VIA_MULTICAST) private var connectRemoteViaMulticast = false
|
||||
@AppStorage(DEFAULT_OFFER_REMOTE_MULTICAST) private var offerRemoteMulticast = true
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@State private var sessionAddress: String = ""
|
||||
@State private var remoteCtrls: [RemoteCtrlInfo] = []
|
||||
@State private var alert: ConnectDesktopAlert?
|
||||
|
||||
private enum ConnectDesktopAlert: Identifiable {
|
||||
case unlinkDesktop(rc: RemoteCtrlInfo)
|
||||
case disconnectDesktop(action: UserDisconnectAction)
|
||||
case badInvitationError
|
||||
case badVersionError(version: String?)
|
||||
case desktopDisconnectedError
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .unlinkDesktop(rc): "unlinkDesktop \(rc.remoteCtrlId)"
|
||||
case let .disconnectDesktop(action): "disconnectDecktop \(action)"
|
||||
case .badInvitationError: "badInvitationError"
|
||||
case let .badVersionError(v): "badVersionError \(v ?? "")"
|
||||
case .desktopDisconnectedError: "desktopDisconnectedError"
|
||||
case let .error(title, _): "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum UserDisconnectAction: String {
|
||||
case back
|
||||
case dismiss // TODO dismiss settings after confirmation
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if viaSettings {
|
||||
viewBody
|
||||
.modifier(BackButton(label: "Back") {
|
||||
if m.activeRemoteCtrl {
|
||||
alert = .disconnectDesktop(action: .back)
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
NavigationView {
|
||||
viewBody
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var viewBody: some View {
|
||||
Group {
|
||||
if let session = m.remoteCtrlSession {
|
||||
switch session.sessionState {
|
||||
case .starting: connectingDesktopView(session, nil)
|
||||
case let .connecting(rc_): connectingDesktopView(session, rc_)
|
||||
case let .pendingConfirmation(rc_, sessCode):
|
||||
if confirmRemoteSessions || rc_ == nil {
|
||||
verifySessionView(session, rc_, sessCode)
|
||||
} else {
|
||||
connectingDesktopView(session, rc_).onAppear {
|
||||
verifyDesktopSessionCode(sessCode)
|
||||
}
|
||||
}
|
||||
case let .connected(rc, _): activeSessionView(session, rc)
|
||||
}
|
||||
} else {
|
||||
connectDesktopView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
setDeviceName(deviceName)
|
||||
updateRemoteCtrls()
|
||||
}
|
||||
.onDisappear {
|
||||
if m.remoteCtrlSession != nil {
|
||||
disconnectDesktop()
|
||||
}
|
||||
}
|
||||
.onChange(of: deviceName) {
|
||||
setDeviceName($0)
|
||||
}
|
||||
.onChange(of: m.activeRemoteCtrl) {
|
||||
UIApplication.shared.isIdleTimerDisabled = $0
|
||||
}
|
||||
.alert(item: $alert) { a in
|
||||
switch a {
|
||||
case let .unlinkDesktop(rc):
|
||||
Alert(
|
||||
title: Text("Unlink desktop?"),
|
||||
primaryButton: .destructive(Text("Unlink")) {
|
||||
unlinkDesktop(rc)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case let .disconnectDesktop(action):
|
||||
Alert(
|
||||
title: Text("Disconnect desktop?"),
|
||||
primaryButton: .destructive(Text("Disconnect")) {
|
||||
disconnectDesktop(action)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case .badInvitationError:
|
||||
Alert(title: Text("Bad desktop address"))
|
||||
case let .badVersionError(v):
|
||||
Alert(
|
||||
title: Text("Incompatible version"),
|
||||
message: Text("Desktop app version \(v ?? "") is not compatible with this app.")
|
||||
)
|
||||
case .desktopDisconnectedError:
|
||||
Alert(title: Text("Connection terminated"))
|
||||
case let .error(title, error):
|
||||
Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled(m.activeRemoteCtrl)
|
||||
}
|
||||
|
||||
private func connectDesktopView() -> some View {
|
||||
List {
|
||||
Section("This device name") {
|
||||
devicesView()
|
||||
}
|
||||
scanDesctopAddressView()
|
||||
if developerTools {
|
||||
desktopAddressView()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connect to desktop")
|
||||
}
|
||||
|
||||
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
|
||||
List {
|
||||
Section("Connecting to desktop") {
|
||||
ctrlDeviceNameText(session, rc)
|
||||
ctrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
if let sessCode = session.sessionCode {
|
||||
Section("Session code") {
|
||||
sessionCodeText(sessCode)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
disconnectButton()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connecting to desktop")
|
||||
}
|
||||
|
||||
private func verifySessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?, _ sessCode: String) -> some View {
|
||||
List {
|
||||
Section("Connected to desktop") {
|
||||
ctrlDeviceNameText(session, rc)
|
||||
ctrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
Section("Verify code with desktop") {
|
||||
sessionCodeText(sessCode)
|
||||
Button {
|
||||
verifyDesktopSessionCode(sessCode)
|
||||
} label: {
|
||||
Label("Confirm", systemImage: "checkmark")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
disconnectButton()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Verify connection")
|
||||
}
|
||||
|
||||
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
|
||||
var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo.deviceName)
|
||||
if (rc == nil) {
|
||||
t = t + Text(" ") + Text("(new)").italic()
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
private func ctrlDeviceVersionText(_ session: RemoteCtrlSession) -> Text {
|
||||
let v = session.ctrlAppInfo.appVersionRange.maxVersion
|
||||
var t = Text("v\(v)")
|
||||
if v != session.appVersion {
|
||||
t = t + Text(" ") + Text("(this device v\(session.appVersion))").italic()
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
private func activeSessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo) -> some View {
|
||||
List {
|
||||
Section("Connected desktop") {
|
||||
Text(rc.deviceViewName)
|
||||
ctrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
if let sessCode = session.sessionCode {
|
||||
Section("Session code") {
|
||||
sessionCodeText(sessCode)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
disconnectButton()
|
||||
} footer: {
|
||||
// This is specific to iOS
|
||||
Text("Keep the app open to use it from desktop")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Connected to desktop")
|
||||
}
|
||||
|
||||
private func sessionCodeText(_ code: String) -> some View {
|
||||
Text(code.prefix(23))
|
||||
}
|
||||
|
||||
private func devicesView() -> some View {
|
||||
Group {
|
||||
TextField("Enter this device name…", text: $deviceName)
|
||||
if !remoteCtrls.isEmpty {
|
||||
NavigationLink {
|
||||
linkedDesktopsView()
|
||||
} label: {
|
||||
Text("Linked desktops")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func scanDesctopAddressView() -> some View {
|
||||
Section("Scan QR code from desktop") {
|
||||
CodeScannerView(codeTypes: [.qr], completion: processDesktopQRCode)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.cornerRadius(12)
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
private func desktopAddressView() -> some View {
|
||||
Section("Desktop address") {
|
||||
if sessionAddress.isEmpty {
|
||||
Button {
|
||||
sessionAddress = UIPasteboard.general.string ?? ""
|
||||
} label: {
|
||||
Label("Paste desktop address", systemImage: "doc.plaintext")
|
||||
}
|
||||
.disabled(!UIPasteboard.general.hasStrings)
|
||||
} else {
|
||||
HStack {
|
||||
Text(sessionAddress).lineLimit(1)
|
||||
Spacer()
|
||||
Image(systemName: "multiply.circle.fill")
|
||||
.foregroundColor(.secondary)
|
||||
.onTapGesture { sessionAddress = "" }
|
||||
}
|
||||
}
|
||||
Button {
|
||||
connectDesktopAddress(sessionAddress)
|
||||
} label: {
|
||||
Label("Connect to desktop", systemImage: "rectangle.connected.to.line.below")
|
||||
}
|
||||
.disabled(sessionAddress.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
private func linkedDesktopsView() -> some View {
|
||||
List {
|
||||
Section("Desktop devices") {
|
||||
ForEach(remoteCtrls, id: \.remoteCtrlId) { rc in
|
||||
remoteCtrlView(rc)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
if let i = indexSet.first, i < remoteCtrls.count {
|
||||
alert = .unlinkDesktop(rc: remoteCtrls[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Linked desktop options") {
|
||||
Toggle("Verify connections", isOn: $confirmRemoteSessions)
|
||||
Toggle("Discover on network", isOn: $connectRemoteViaMulticast).disabled(true)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Linked desktops")
|
||||
}
|
||||
|
||||
private func remoteCtrlView(_ rc: RemoteCtrlInfo) -> some View {
|
||||
Text(rc.deviceViewName)
|
||||
}
|
||||
|
||||
|
||||
private func setDeviceName(_ name: String) {
|
||||
do {
|
||||
try setLocalDeviceName(deviceName)
|
||||
} catch let e {
|
||||
errorAlert(e)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRemoteCtrls() {
|
||||
do {
|
||||
remoteCtrls = try listRemoteCtrls()
|
||||
} catch let e {
|
||||
errorAlert(e)
|
||||
}
|
||||
}
|
||||
|
||||
private func processDesktopQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||
switch resp {
|
||||
case let .success(r): connectDesktopAddress(r.string)
|
||||
case let .failure(e): errorAlert(e)
|
||||
}
|
||||
}
|
||||
|
||||
private func connectDesktopAddress(_ addr: String) {
|
||||
Task {
|
||||
do {
|
||||
let (rc_, ctrlAppInfo, v) = try await connectRemoteCtrl(desktopAddress: addr)
|
||||
await MainActor.run {
|
||||
sessionAddress = ""
|
||||
m.remoteCtrlSession = RemoteCtrlSession(
|
||||
ctrlAppInfo: ctrlAppInfo,
|
||||
appVersion: v,
|
||||
sessionState: .connecting(remoteCtrl_: rc_)
|
||||
)
|
||||
}
|
||||
} catch let e {
|
||||
await MainActor.run {
|
||||
switch e as? ChatResponse {
|
||||
case .chatCmdError(_, .errorRemoteCtrl(.badInvitation)): alert = .badInvitationError
|
||||
case .chatCmdError(_, .error(.commandError)): alert = .badInvitationError
|
||||
case let .chatCmdError(_, .errorRemoteCtrl(.badVersion(v))): alert = .badVersionError(version: v)
|
||||
case .chatCmdError(_, .errorAgent(.RCP(.version))): alert = .badVersionError(version: nil)
|
||||
case .chatCmdError(_, .errorAgent(.RCP(.ctrlAuth))): alert = .desktopDisconnectedError
|
||||
default: errorAlert(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func verifyDesktopSessionCode(_ sessCode: String) {
|
||||
Task {
|
||||
do {
|
||||
let rc = try await verifyRemoteCtrlSession(sessCode)
|
||||
await MainActor.run {
|
||||
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(.connected(remoteCtrl: rc, sessionCode: sessCode))
|
||||
}
|
||||
await MainActor.run {
|
||||
updateRemoteCtrls()
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
errorAlert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func disconnectButton() -> some View {
|
||||
Button {
|
||||
disconnectDesktop()
|
||||
} label: {
|
||||
Label("Disconnect", systemImage: "multiply")
|
||||
}
|
||||
}
|
||||
|
||||
private func disconnectDesktop(_ action: UserDisconnectAction? = nil) {
|
||||
Task {
|
||||
do {
|
||||
try await stopRemoteCtrl()
|
||||
await MainActor.run {
|
||||
switchToLocalSession()
|
||||
switch action {
|
||||
case .back: dismiss()
|
||||
case .dismiss: dismiss()
|
||||
case .none: ()
|
||||
}
|
||||
}
|
||||
} catch let e {
|
||||
await MainActor.run {
|
||||
errorAlert(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func unlinkDesktop(_ rc: RemoteCtrlInfo) {
|
||||
Task {
|
||||
do {
|
||||
try await deleteRemoteCtrl(rc.remoteCtrlId)
|
||||
await MainActor.run {
|
||||
remoteCtrls.removeAll(where: { $0.remoteCtrlId == rc.remoteCtrlId })
|
||||
}
|
||||
} catch let e {
|
||||
await MainActor.run {
|
||||
errorAlert(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func errorAlert(_ error: Error) {
|
||||
let a = getErrorAlert(error, "Error")
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ConnectDesktopView()
|
||||
}
|
@ -53,6 +53,10 @@ let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion"
|
||||
let DEFAULT_ONBOARDING_STAGE = "onboardingStage"
|
||||
let DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME = "customDisappearingMessageTime"
|
||||
let DEFAULT_SHOW_UNREAD_AND_FAVORITES = "showUnreadAndFavorites"
|
||||
let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess"
|
||||
let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions"
|
||||
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
|
||||
let DEFAULT_OFFER_REMOTE_MULTICAST = "offerRemoteMulticast"
|
||||
|
||||
let appDefaults: [String: Any] = [
|
||||
DEFAULT_SHOW_LA_NOTICE: false,
|
||||
@ -85,7 +89,10 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_SHOW_MUTE_PROFILE_ALERT: true,
|
||||
DEFAULT_ONBOARDING_STAGE: OnboardingStage.onboardingComplete.rawValue,
|
||||
DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME: 300,
|
||||
DEFAULT_SHOW_UNREAD_AND_FAVORITES: false
|
||||
DEFAULT_SHOW_UNREAD_AND_FAVORITES: false,
|
||||
DEFAULT_CONFIRM_REMOTE_SESSIONS: false,
|
||||
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: false,
|
||||
DEFAULT_OFFER_REMOTE_MULTICAST: true
|
||||
]
|
||||
|
||||
enum SimpleXLinkMode: String, Identifiable {
|
||||
@ -178,6 +185,12 @@ struct SettingsView: View {
|
||||
} label: {
|
||||
settingsRow("switch.2") { Text("Chat preferences") }
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
ConnectDesktopView(viaSettings: true)
|
||||
} label: {
|
||||
settingsRow("desktopcomputer") { Text("Use from desktop") }
|
||||
}
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
@ -362,7 +375,9 @@ struct SettingsView: View {
|
||||
|
||||
func settingsRow<Content : View>(_ icon: String, color: Color = .secondary, content: @escaping () -> Content) -> some View {
|
||||
ZStack(alignment: .leading) {
|
||||
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(color)
|
||||
Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center)
|
||||
.symbolRenderingMode(.monochrome)
|
||||
.foregroundColor(color)
|
||||
content().padding(.leading, indent)
|
||||
}
|
||||
}
|
||||
|
@ -18,5 +18,9 @@
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)chat.simplex.app</string>
|
||||
</array>
|
||||
<key>com.apple.developer.networking.multicast</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.device-information.user-assigned-device-name</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -39,6 +39,7 @@
|
||||
5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C36027227F47AD5009F19D9 /* AppDelegate.swift */; };
|
||||
5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */; };
|
||||
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */; };
|
||||
5C3CCFCC2AE6BD3100C3F0C3 /* ConnectDesktopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3CCFCB2AE6BD3100C3F0C3 /* ConnectDesktopView.swift */; };
|
||||
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
|
||||
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
|
||||
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; };
|
||||
@ -117,6 +118,11 @@
|
||||
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
|
||||
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
|
||||
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; };
|
||||
5CDA5A2D2B04FE2D00A71D61 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDA5A282B04FE2D00A71D61 /* libgmp.a */; };
|
||||
5CDA5A2E2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDA5A292B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a */; };
|
||||
5CDA5A2F2B04FE2D00A71D61 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDA5A2A2B04FE2D00A71D61 /* libffi.a */; };
|
||||
5CDA5A302B04FE2D00A71D61 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDA5A2B2B04FE2D00A71D61 /* libgmpxx.a */; };
|
||||
5CDA5A312B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CDA5A2C2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a */; };
|
||||
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
|
||||
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
|
||||
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
@ -142,11 +148,6 @@
|
||||
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
|
||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
|
||||
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; };
|
||||
5CF4DF772AFF8D4E007893ED /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF4DF722AFF8D4D007893ED /* libffi.a */; };
|
||||
5CF4DF782AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF4DF732AFF8D4D007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a */; };
|
||||
5CF4DF792AFF8D4E007893ED /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF4DF742AFF8D4D007893ED /* libgmpxx.a */; };
|
||||
5CF4DF7A2AFF8D4E007893ED /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF4DF752AFF8D4E007893ED /* libgmp.a */; };
|
||||
5CF4DF7B2AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF4DF762AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a */; };
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
|
||||
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; };
|
||||
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
@ -287,6 +288,7 @@
|
||||
5C36027227F47AD5009F19D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineWidth.swift; sourceTree = "<group>"; };
|
||||
5C3A88D027DF57800060F1C2 /* FramedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramedItemView.swift; sourceTree = "<group>"; };
|
||||
5C3CCFCB2AE6BD3100C3F0C3 /* ConnectDesktopView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectDesktopView.swift; sourceTree = "<group>"; };
|
||||
5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegrityErrorItemView.swift; sourceTree = "<group>"; };
|
||||
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; };
|
||||
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
|
||||
@ -402,6 +404,11 @@
|
||||
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; };
|
||||
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; };
|
||||
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; };
|
||||
5CDA5A282B04FE2D00A71D61 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CDA5A292B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
5CDA5A2A2B04FE2D00A71D61 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CDA5A2B2B04FE2D00A71D61 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CDA5A2C2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a"; sourceTree = "<group>"; };
|
||||
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5CDCAD472818589900503DA2 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@ -428,11 +435,6 @@
|
||||
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
|
||||
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
|
||||
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
|
||||
5CF4DF722AFF8D4D007893ED /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CF4DF732AFF8D4D007893ED /* 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>"; };
|
||||
5CF4DF742AFF8D4D007893ED /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CF4DF752AFF8D4E007893ED /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CF4DF762AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a"; sourceTree = "<group>"; };
|
||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
|
||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
|
||||
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
|
||||
@ -515,13 +517,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CDA5A302B04FE2D00A71D61 /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5CF4DF792AFF8D4E007893ED /* libgmpxx.a in Frameworks */,
|
||||
5CF4DF772AFF8D4E007893ED /* libffi.a in Frameworks */,
|
||||
5CDA5A2D2B04FE2D00A71D61 /* libgmp.a in Frameworks */,
|
||||
5CDA5A2E2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a in Frameworks */,
|
||||
5CDA5A2F2B04FE2D00A71D61 /* libffi.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5CF4DF7A2AFF8D4E007893ED /* libgmp.a in Frameworks */,
|
||||
5CF4DF7B2AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a in Frameworks */,
|
||||
5CF4DF782AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a in Frameworks */,
|
||||
5CDA5A312B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -554,6 +556,7 @@
|
||||
5CB924DD27A8622200ACCCDD /* NewChat */,
|
||||
5CFA59C22860B04D00863A68 /* Database */,
|
||||
5CB634AB29E46CDB0066AD6B /* LocalAuth */,
|
||||
5CA8D01B2AD9B076001FD661 /* RemoteAccess */,
|
||||
5CB924DF27A8678B00ACCCDD /* UserSettings */,
|
||||
5C2E261127A30FEA00F70299 /* TerminalView.swift */,
|
||||
);
|
||||
@ -582,11 +585,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CF4DF722AFF8D4D007893ED /* libffi.a */,
|
||||
5CF4DF752AFF8D4E007893ED /* libgmp.a */,
|
||||
5CF4DF742AFF8D4D007893ED /* libgmpxx.a */,
|
||||
5CF4DF732AFF8D4D007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8-ghc9.6.3.a */,
|
||||
5CF4DF762AFF8D4E007893ED /* libHSsimplex-chat-5.4.0.3-EnhmkSQK6HvJ11g1uZERg8.a */,
|
||||
5CDA5A2A2B04FE2D00A71D61 /* libffi.a */,
|
||||
5CDA5A282B04FE2D00A71D61 /* libgmp.a */,
|
||||
5CDA5A2B2B04FE2D00A71D61 /* libgmpxx.a */,
|
||||
5CDA5A292B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL-ghc9.6.3.a */,
|
||||
5CDA5A2C2B04FE2D00A71D61 /* libHSsimplex-chat-5.4.0.3-rODxCBVsb2BkD1fnTAqXL.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@ -694,6 +697,14 @@
|
||||
path = "Tests iOS";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5CA8D01B2AD9B076001FD661 /* RemoteAccess */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C3CCFCB2AE6BD3100C3F0C3 /* ConnectDesktopView.swift */,
|
||||
);
|
||||
path = RemoteAccess;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5CB0BA8C282711BC00B3292C /* Onboarding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1180,6 +1191,7 @@
|
||||
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */,
|
||||
5C5DB70E289ABDD200730FFF /* AppearanceSettings.swift in Sources */,
|
||||
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */,
|
||||
5C3CCFCC2AE6BD3100C3F0C3 /* ConnectDesktopView.swift in Sources */,
|
||||
5C9C2DA92899DA6F00CC63B1 /* NetworkAndServers.swift in Sources */,
|
||||
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */,
|
||||
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */,
|
||||
@ -1500,6 +1512,7 @@
|
||||
INFOPLIST_FILE = "SimpleX--iOS--Info.plist";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
|
||||
INFOPLIST_KEY_NSFaceIDUsageDescription = "SimpleX uses Face ID for local authentication";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SimpleX needs access to Photo Library for saving captured and received media";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
@ -1542,6 +1555,7 @@
|
||||
INFOPLIST_FILE = "SimpleX--iOS--Info.plist";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
|
||||
INFOPLIST_KEY_NSFaceIDUsageDescription = "SimpleX uses Face ID for local authentication";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network.";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SimpleX needs access to Photo Library for saving captured and received media";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
|
@ -120,6 +120,16 @@ public enum ChatCommand {
|
||||
case receiveFile(fileId: Int64, encrypted: Bool?, inline: Bool?)
|
||||
case setFileToReceive(fileId: Int64, encrypted: Bool?)
|
||||
case cancelFile(fileId: Int64)
|
||||
// remote desktop commands
|
||||
case setLocalDeviceName(displayName: String)
|
||||
case connectRemoteCtrl(xrcpInvitation: String)
|
||||
case findKnownRemoteCtrl
|
||||
case confirmRemoteCtrl(remoteCtrlId: Int64)
|
||||
case verifyRemoteCtrlSession(sessionCode: String)
|
||||
case listRemoteCtrls
|
||||
case stopRemoteCtrl
|
||||
case deleteRemoteCtrl(remoteCtrlId: Int64)
|
||||
// misc
|
||||
case showVersion
|
||||
case string(String)
|
||||
|
||||
@ -260,6 +270,14 @@ public enum ChatCommand {
|
||||
case let .receiveFile(fileId, encrypt, inline): return "/freceive \(fileId)\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))"
|
||||
case let .setFileToReceive(fileId, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("encrypt", encrypt))"
|
||||
case let .cancelFile(fileId): return "/fcancel \(fileId)"
|
||||
case let .setLocalDeviceName(displayName): return "/set device name \(displayName)"
|
||||
case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)"
|
||||
case .findKnownRemoteCtrl: return "/find remote ctrl"
|
||||
case let .confirmRemoteCtrl(rcId): return "/confirm remote ctrl \(rcId)"
|
||||
case let .verifyRemoteCtrlSession(sessCode): return "/verify remote ctrl \(sessCode)"
|
||||
case .listRemoteCtrls: return "/list remote ctrls"
|
||||
case .stopRemoteCtrl: return "/stop remote ctrl"
|
||||
case let .deleteRemoteCtrl(rcId): return "/delete remote ctrl \(rcId)"
|
||||
case .showVersion: return "/version"
|
||||
case let .string(str): return str
|
||||
}
|
||||
@ -375,6 +393,14 @@ public enum ChatCommand {
|
||||
case .receiveFile: return "receiveFile"
|
||||
case .setFileToReceive: return "setFileToReceive"
|
||||
case .cancelFile: return "cancelFile"
|
||||
case .setLocalDeviceName: return "setLocalDeviceName"
|
||||
case .connectRemoteCtrl: return "connectRemoteCtrl"
|
||||
case .findKnownRemoteCtrl: return "findKnownRemoteCtrl"
|
||||
case .confirmRemoteCtrl: return "confirmRemoteCtrl"
|
||||
case .verifyRemoteCtrlSession: return "verifyRemoteCtrlSession"
|
||||
case .listRemoteCtrls: return "listRemoteCtrls"
|
||||
case .stopRemoteCtrl: return "stopRemoteCtrl"
|
||||
case .deleteRemoteCtrl: return "deleteRemoteCtrl"
|
||||
case .showVersion: return "showVersion"
|
||||
case .string: return "console command"
|
||||
}
|
||||
@ -581,6 +607,14 @@ public enum ChatResponse: Decodable, Error {
|
||||
case ntfMessages(user_: User?, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
||||
case newContactConnection(user: UserRef, connection: PendingContactConnection)
|
||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||
// remote desktop responses/events
|
||||
case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo])
|
||||
case remoteCtrlFound(remoteCtrl: RemoteCtrlInfo)
|
||||
case remoteCtrlConnecting(remoteCtrl_: RemoteCtrlInfo?, ctrlAppInfo: CtrlAppInfo, appVersion: String)
|
||||
case remoteCtrlSessionCode(remoteCtrl_: RemoteCtrlInfo?, sessionCode: String)
|
||||
case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo)
|
||||
case remoteCtrlStopped
|
||||
// misc
|
||||
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
|
||||
case cmdOk(user: UserRef?)
|
||||
case chatCmdError(user_: UserRef?, chatError: ChatError)
|
||||
@ -720,6 +754,12 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .ntfMessages: return "ntfMessages"
|
||||
case .newContactConnection: return "newContactConnection"
|
||||
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
||||
case .remoteCtrlList: return "remoteCtrlList"
|
||||
case .remoteCtrlFound: return "remoteCtrlFound"
|
||||
case .remoteCtrlConnecting: return "remoteCtrlConnecting"
|
||||
case .remoteCtrlSessionCode: return "remoteCtrlSessionCode"
|
||||
case .remoteCtrlConnected: return "remoteCtrlConnected"
|
||||
case .remoteCtrlStopped: return "remoteCtrlStopped"
|
||||
case .versionInfo: return "versionInfo"
|
||||
case .cmdOk: return "cmdOk"
|
||||
case .chatCmdError: return "chatCmdError"
|
||||
@ -862,6 +902,12 @@ public enum ChatResponse: Decodable, Error {
|
||||
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 .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls)
|
||||
case let .remoteCtrlFound(remoteCtrl): return String(describing: remoteCtrl)
|
||||
case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)"
|
||||
case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)"
|
||||
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
|
||||
case .remoteCtrlStopped: return noDetails
|
||||
case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))"
|
||||
case .cmdOk: return noDetails
|
||||
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
@ -1488,6 +1534,33 @@ public enum NotificationPreviewMode: String, SelectableItem {
|
||||
public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden]
|
||||
}
|
||||
|
||||
public struct RemoteCtrlInfo: Decodable {
|
||||
public var remoteCtrlId: Int64
|
||||
public var ctrlDeviceName: String
|
||||
public var sessionState: RemoteCtrlSessionState?
|
||||
|
||||
public var deviceViewName: String {
|
||||
ctrlDeviceName == "" ? "\(remoteCtrlId)" : ctrlDeviceName
|
||||
}
|
||||
}
|
||||
|
||||
public enum RemoteCtrlSessionState: Decodable {
|
||||
case starting
|
||||
case connecting
|
||||
case pendingConfirmation(sessionCode: String)
|
||||
case connected(sessionCode: String)
|
||||
}
|
||||
|
||||
public struct CtrlAppInfo: Decodable {
|
||||
public var appVersionRange: AppVersionRange
|
||||
public var deviceName: String
|
||||
}
|
||||
|
||||
public struct AppVersionRange: Decodable {
|
||||
public var minVersion: String
|
||||
public var maxVersion: String
|
||||
}
|
||||
|
||||
public struct CoreVersionInfo: Decodable {
|
||||
public var version: String
|
||||
public var simplexmqVersion: String
|
||||
@ -1515,6 +1588,7 @@ public enum ChatError: Decodable {
|
||||
case errorAgent(agentError: AgentErrorType)
|
||||
case errorStore(storeError: StoreError)
|
||||
case errorDatabase(databaseError: DatabaseError)
|
||||
case errorRemoteCtrl(remoteCtrlError: RemoteCtrlError)
|
||||
case invalidJSON(json: String)
|
||||
}
|
||||
|
||||
@ -1674,6 +1748,7 @@ public enum AgentErrorType: Decodable {
|
||||
case SMP(smpErr: ProtocolErrorType)
|
||||
case NTF(ntfErr: ProtocolErrorType)
|
||||
case XFTP(xftpErr: XFTPErrorType)
|
||||
case RCP(rcpErr: RCErrorType)
|
||||
case BROKER(brokerAddress: String, brokerErr: BrokerErrorType)
|
||||
case AGENT(agentErr: SMPAgentError)
|
||||
case INTERNAL(internalErr: String)
|
||||
@ -1731,6 +1806,22 @@ public enum XFTPErrorType: Decodable {
|
||||
case INTERNAL
|
||||
}
|
||||
|
||||
public enum RCErrorType: Decodable {
|
||||
case `internal`(internalErr: String)
|
||||
case identity
|
||||
case noLocalAddress
|
||||
case tlsStartFailed
|
||||
case exception(exception: String)
|
||||
case ctrlAuth
|
||||
case ctrlNotFound
|
||||
case ctrlError(ctrlErr: String)
|
||||
case version
|
||||
case encrypt
|
||||
case decrypt
|
||||
case blockSize
|
||||
case syntax(syntaxErr: String)
|
||||
}
|
||||
|
||||
public enum ProtocolCommandError: Decodable {
|
||||
case UNKNOWN
|
||||
case SYNTAX
|
||||
@ -1766,3 +1857,14 @@ public enum ArchiveError: Decodable {
|
||||
case `import`(chatError: ChatError)
|
||||
case importFile(file: String, chatError: ChatError)
|
||||
}
|
||||
|
||||
public enum RemoteCtrlError: Decodable {
|
||||
case inactive
|
||||
case badState
|
||||
case busy
|
||||
case timeout
|
||||
case disconnected(remoteCtrlId: Int64, reason: String)
|
||||
case badInvitation
|
||||
case badVersion(appVersion: String)
|
||||
// case protocolError(protocolError: RemoteProtocolError)
|
||||
}
|
||||
|
@ -15,7 +15,6 @@
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
@ -126,7 +126,7 @@ fun processIntent(intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
"android.intent.action.VIEW" -> {
|
||||
val uri = intent.data
|
||||
if (uri != null) connectIfOpenedViaUri(uri.toURI(), ChatModel)
|
||||
if (uri != null) connectIfOpenedViaUri(chatModel.remoteHostId, uri.toURI(), ChatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
updatingChatsMutex.withLock {
|
||||
kotlin.runCatching {
|
||||
val currentUserId = chatModel.currentUser.value?.userId
|
||||
val chats = ArrayList(chatController.apiGetChats())
|
||||
val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId))
|
||||
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */
|
||||
if (chatModel.currentUser.value?.userId == currentUserId) {
|
||||
val currentChatId = chatModel.chatId.value
|
||||
|
@ -16,6 +16,8 @@ import kotlin.random.Random
|
||||
|
||||
actual val appPlatform = AppPlatform.ANDROID
|
||||
|
||||
actual val deviceName = android.os.Build.MODEL
|
||||
|
||||
var isAppOnForeground: Boolean = false
|
||||
|
||||
@Suppress("ConstantLocale")
|
||||
|
@ -23,6 +23,8 @@ actual val agentDatabaseFileName: String = "files_agent.db"
|
||||
|
||||
actual val databaseExportDir: File = androidAppContext.cacheDir
|
||||
|
||||
actual val remoteHostsDir: File = File(tmpDir.absolutePath + File.separator + "remote_hosts")
|
||||
|
||||
actual fun desktopOpenDatabaseDir() {}
|
||||
|
||||
@Composable
|
||||
|
@ -38,7 +38,7 @@ actual class RecorderNative: RecorderInterface {
|
||||
rec.setAudioSamplingRate(16000)
|
||||
rec.setAudioEncodingBitRate(32000)
|
||||
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
|
||||
val fileToSave = File.createTempFile(generateNewFileName("voice", "${RecorderInterface.extension}_"), ".tmp", tmpDir)
|
||||
val fileToSave = File.createTempFile(generateNewFileName("voice", "${RecorderInterface.extension}_", tmpDir), ".tmp", tmpDir)
|
||||
fileToSave.deleteOnExit()
|
||||
val path = fileToSave.absolutePath
|
||||
filePath = path
|
||||
|
@ -115,22 +115,23 @@ actual fun ActiveCallView() {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) {
|
||||
Log.d(TAG, "has active call $call")
|
||||
val callRh = call.remoteHostId
|
||||
when (val r = apiMsg.resp) {
|
||||
is WCallResponse.Capabilities -> withBGApi {
|
||||
val callType = CallType(call.localMedia, r.capabilities)
|
||||
chatModel.controller.apiSendCallInvitation(call.contact, callType)
|
||||
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
|
||||
}
|
||||
is WCallResponse.Offer -> withBGApi {
|
||||
chatModel.controller.apiSendCallOffer(call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
|
||||
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
|
||||
}
|
||||
is WCallResponse.Answer -> withBGApi {
|
||||
chatModel.controller.apiSendCallAnswer(call.contact, r.answer, r.iceCandidates)
|
||||
chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
|
||||
}
|
||||
is WCallResponse.Ice -> withBGApi {
|
||||
chatModel.controller.apiSendCallExtraInfo(call.contact, r.iceCandidates)
|
||||
chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates)
|
||||
}
|
||||
is WCallResponse.Connection ->
|
||||
try {
|
||||
@ -139,7 +140,7 @@ actual fun ActiveCallView() {
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectedAt = Clock.System.now())
|
||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||
}
|
||||
withBGApi { chatModel.controller.apiCallStatus(call.contact, callStatus) }
|
||||
withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) }
|
||||
} catch (e: Error) {
|
||||
Log.d(TAG,"call status ${r.state.connectionState} not used")
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
package chat.simplex.common.views.chat
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import chat.simplex.common.views.chat.ScanCodeLayout
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@Composable
|
||||
actual fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
ScanCodeLayout(verifyCode, close)
|
||||
}
|
@ -167,7 +167,7 @@ actual fun getAppFileUri(fileName: String): URI =
|
||||
FileProvider.getUriForFile(androidAppContext, "$APPLICATION_ID.provider", if (File(fileName).isAbsolute) File(fileName) else File(getAppFilePath(fileName))).toURI()
|
||||
|
||||
// https://developer.android.com/training/data-storage/shared/documents-files#bitmap
|
||||
actual fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? {
|
||||
actual suspend fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? {
|
||||
val filePath = getLoadedFilePath(file)
|
||||
return if (filePath != null && file != null) {
|
||||
try {
|
||||
@ -295,7 +295,7 @@ actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean)
|
||||
return try {
|
||||
val ext = if (asPng) "png" else "jpg"
|
||||
tmpDir.mkdir()
|
||||
return File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", ext)).apply {
|
||||
return File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", ext, tmpDir)).apply {
|
||||
outputStream().use { out ->
|
||||
image.asAndroidBitmap().compress(if (asPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 85, out)
|
||||
out.flush()
|
||||
|
@ -12,7 +12,8 @@ import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
actual fun ConnectViaLinkView(m: ChatModel, close: () -> Unit) {
|
||||
actual fun ConnectViaLinkView(m: ChatModel, rhId: Long?, close: () -> Unit) {
|
||||
// TODO this should close if remote host changes in model
|
||||
val selection = remember {
|
||||
mutableStateOf(
|
||||
runCatching { ConnectViaLinkTab.valueOf(m.controller.appPrefs.connectViaLinkTab.get()!!) }.getOrDefault(ConnectViaLinkTab.SCAN)
|
||||
@ -31,10 +32,10 @@ actual fun ConnectViaLinkView(m: ChatModel, close: () -> Unit) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
when (selection.value) {
|
||||
ConnectViaLinkTab.SCAN -> {
|
||||
ScanToConnectView(m, close)
|
||||
ScanToConnectView(m, rhId, close)
|
||||
}
|
||||
ConnectViaLinkTab.PASTE -> {
|
||||
PasteToConnectView(m, close)
|
||||
PasteToConnectView(m, rhId, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package chat.simplex.common.views.newchat
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
@ -19,6 +20,7 @@ import boofcv.android.ConvertCameraImage
|
||||
import boofcv.factory.fiducial.FactoryFiducial
|
||||
import boofcv.struct.image.GrayU8
|
||||
import chat.simplex.common.platform.TAG
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.util.concurrent.*
|
||||
|
||||
@ -26,6 +28,10 @@ import java.util.concurrent.*
|
||||
|
||||
@Composable
|
||||
actual fun QRCodeScanner(onBarcode: (String) -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
var preview by remember { mutableStateOf<Preview?>(null) }
|
||||
|
@ -7,13 +7,14 @@ import chat.simplex.common.model.ChatModel
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@Composable
|
||||
actual fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
|
||||
actual fun ScanToConnectView(chatModel: ChatModel, rhId: Long?, close: () -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
ConnectContactLayout(
|
||||
chatModel = chatModel,
|
||||
rhId = rhId,
|
||||
incognitoPref = chatModel.controller.appPrefs.incognito,
|
||||
close = close
|
||||
)
|
||||
|
@ -7,10 +7,10 @@ import chat.simplex.common.model.ServerCfg
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@Composable
|
||||
actual fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) {
|
||||
actual fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
ScanProtocolServerLayout(onNext)
|
||||
ScanProtocolServerLayout(rhId, onNext)
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ typedef long* chat_ctrl;
|
||||
|
||||
extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
|
||||
extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
|
||||
extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated
|
||||
extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
|
||||
extern char *chat_parse_markdown(const char *str);
|
||||
@ -86,6 +87,14 @@ Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, __unused jclas
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jint rhId, jstring msg) {
|
||||
const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
|
||||
jstring res = (*env)->NewStringUTF(env, chat_send_remote_cmd((void*)controller, rhId, _msg));
|
||||
(*env)->ReleaseStringUTFChars(env, msg, _msg);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) {
|
||||
return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller));
|
||||
|
@ -16,6 +16,7 @@ typedef long* chat_ctrl;
|
||||
|
||||
extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
|
||||
extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
|
||||
extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated
|
||||
extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
|
||||
extern char *chat_parse_markdown(const char *str);
|
||||
@ -98,6 +99,14 @@ Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, jclass clazz,
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmd(JNIEnv *env, jclass clazz, jlong controller, jint rhId, jstring msg) {
|
||||
const char *_msg = encode_to_utf8_chars(env, msg);
|
||||
jstring res = decode_to_utf8_string(env, chat_send_remote_cmd((void*)controller, rhId, _msg));
|
||||
(*env)->ReleaseStringUTFChars(env, msg, _msg);
|
||||
return res;
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_chat_simplex_common_platform_CoreKt_chatRecvMsg(JNIEnv *env, jclass clazz, jlong controller) {
|
||||
return decode_to_utf8_string(env, chat_recv_msg((void*)controller));
|
||||
|
@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.*
|
||||
data class SettingsViewState(
|
||||
val userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||
val scaffoldState: ScaffoldState,
|
||||
val switchingUsers: MutableState<Boolean>
|
||||
val switchingUsersAndHosts: MutableState<Boolean>
|
||||
)
|
||||
|
||||
@Composable
|
||||
@ -121,8 +121,8 @@ fun MainScreen() {
|
||||
showAdvertiseLAAlert = true
|
||||
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val switchingUsers = rememberSaveable { mutableStateOf(false) }
|
||||
val settingsState = remember { SettingsViewState(userPickerState, scaffoldState, switchingUsers) }
|
||||
val switchingUsersAndHosts = rememberSaveable { mutableStateOf(false) }
|
||||
val settingsState = remember { SettingsViewState(userPickerState, scaffoldState, switchingUsersAndHosts) }
|
||||
if (appPlatform.isAndroid) {
|
||||
AndroidScreen(settingsState)
|
||||
} else {
|
||||
@ -138,7 +138,7 @@ fun MainScreen() {
|
||||
}
|
||||
onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {}
|
||||
onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel)
|
||||
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel)
|
||||
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel, null)
|
||||
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
@ -298,7 +298,7 @@ fun DesktopScreen(settingsState: SettingsViewState) {
|
||||
EndPartOfScreen()
|
||||
}
|
||||
}
|
||||
val (userPickerState, scaffoldState, switchingUsers ) = settingsState
|
||||
val (userPickerState, scaffoldState, switchingUsersAndHosts ) = settingsState
|
||||
val scope = rememberCoroutineScope()
|
||||
if (scaffoldState.drawerState.isOpen) {
|
||||
Box(
|
||||
@ -312,8 +312,9 @@ fun DesktopScreen(settingsState: SettingsViewState) {
|
||||
)
|
||||
}
|
||||
VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH))
|
||||
UserPicker(chatModel, userPickerState, switchingUsers) {
|
||||
UserPicker(chatModel, userPickerState, switchingUsersAndHosts) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
ModalManager.fullscreen.showInView()
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
package chat.simplex.common.model
|
||||
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
@ -26,6 +27,7 @@ import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.*
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
@ -106,6 +108,13 @@ object ChatModel {
|
||||
|
||||
var updatingChatsMutex: Mutex = Mutex()
|
||||
|
||||
// remote controller
|
||||
val remoteHosts = mutableStateListOf<RemoteHostInfo>()
|
||||
val currentRemoteHost = mutableStateOf<RemoteHostInfo?>(null)
|
||||
val remoteHostId: Long? get() = currentRemoteHost?.value?.remoteHostId
|
||||
val newRemoteHostPairing = mutableStateOf<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null)
|
||||
val remoteCtrlSession = mutableStateOf<RemoteCtrlSession?>(null)
|
||||
|
||||
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
|
||||
currentUser.value
|
||||
} else {
|
||||
@ -133,16 +142,17 @@ object ChatModel {
|
||||
}
|
||||
|
||||
// toList() here is to prevent ConcurrentModificationException that is rarely happens but happens
|
||||
fun hasChat(id: String): Boolean = chats.toList().firstOrNull { it.id == id } != null
|
||||
fun hasChat(rhId: Long?, id: String): Boolean = chats.toList().firstOrNull { it.id == id && it.remoteHostId == rhId } != null
|
||||
// TODO pass rhId?
|
||||
fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id }
|
||||
fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
|
||||
fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId }
|
||||
fun getGroupMember(groupMemberId: Long): GroupMember? = groupMembers.firstOrNull { it.groupMemberId == groupMemberId }
|
||||
private fun getChatIndex(id: String): Int = chats.toList().indexOfFirst { it.id == id }
|
||||
private fun getChatIndex(rhId: Long?, id: String): Int = chats.toList().indexOfFirst { it.id == id && it.remoteHostId == rhId }
|
||||
fun addChat(chat: Chat) = chats.add(index = 0, chat)
|
||||
|
||||
fun updateChatInfo(cInfo: ChatInfo) {
|
||||
val i = getChatIndex(cInfo.id)
|
||||
fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) {
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
if (i >= 0) {
|
||||
val currentCInfo = chats[i].chatInfo
|
||||
var newCInfo = cInfo
|
||||
@ -164,23 +174,23 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateContactConnection(contactConnection: PendingContactConnection) = updateChat(ChatInfo.ContactConnection(contactConnection))
|
||||
fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection))
|
||||
|
||||
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
|
||||
fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
|
||||
|
||||
fun updateContactConnectionStats(contact: Contact, connectionStats: ConnectionStats) {
|
||||
fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) {
|
||||
val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats)
|
||||
val updatedContact = contact.copy(activeConn = updatedConn)
|
||||
updateContact(updatedContact)
|
||||
updateContact(rhId, updatedContact)
|
||||
}
|
||||
|
||||
fun updateGroup(groupInfo: GroupInfo) = updateChat(ChatInfo.Group(groupInfo))
|
||||
fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo))
|
||||
|
||||
private fun updateChat(cInfo: ChatInfo, addMissing: Boolean = true) {
|
||||
if (hasChat(cInfo.id)) {
|
||||
updateChatInfo(cInfo)
|
||||
private fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) {
|
||||
if (hasChat(rhId, cInfo.id)) {
|
||||
updateChatInfo(rhId, cInfo)
|
||||
} else if (addMissing) {
|
||||
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf()))
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,8 +205,8 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceChat(id: String, chat: Chat) {
|
||||
val i = getChatIndex(id)
|
||||
fun replaceChat(rhId: Long?, id: String, chat: Chat) {
|
||||
val i = getChatIndex(rhId, id)
|
||||
if (i >= 0) {
|
||||
chats[i] = chat
|
||||
} else {
|
||||
@ -205,9 +215,9 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock {
|
||||
suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock {
|
||||
// update previews
|
||||
val i = getChatIndex(cInfo.id)
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
@ -216,7 +226,7 @@ object ChatModel {
|
||||
chatStats =
|
||||
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
|
||||
val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId
|
||||
increaseUnreadCounter(currentUser.value!!)
|
||||
increaseUnreadCounter(rhId, currentUser.value!!)
|
||||
chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, minUnreadItemId = minUnreadId)
|
||||
}
|
||||
else
|
||||
@ -226,7 +236,7 @@ object ChatModel {
|
||||
popChat_(i)
|
||||
}
|
||||
} else {
|
||||
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
}
|
||||
Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||
withContext(Dispatchers.Main) {
|
||||
@ -246,9 +256,9 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock {
|
||||
suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock {
|
||||
// update previews
|
||||
val i = getChatIndex(cInfo.id)
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
val res: Boolean
|
||||
if (i >= 0) {
|
||||
@ -258,12 +268,12 @@ object ChatModel {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
|
||||
if (pItem.isRcvNew && !cItem.isRcvNew) {
|
||||
// status changed from New to Read, update counter
|
||||
decreaseCounterInChat(cInfo.id)
|
||||
decreaseCounterInChat(rhId, cInfo.id)
|
||||
}
|
||||
}
|
||||
res = false
|
||||
} else {
|
||||
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
res = true
|
||||
}
|
||||
Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||
@ -305,12 +315,12 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChatItem(cInfo: ChatInfo, cItem: ChatItem) {
|
||||
fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
if (cItem.isRcvNew) {
|
||||
decreaseCounterInChat(cInfo.id)
|
||||
decreaseCounterInChat(rhId, cInfo.id)
|
||||
}
|
||||
// update previews
|
||||
val i = getChatIndex(cInfo.id)
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
@ -329,11 +339,11 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun clearChat(cInfo: ChatInfo) {
|
||||
fun clearChat(rhId: Long?, cInfo: ChatInfo) {
|
||||
// clear preview
|
||||
val i = getChatIndex(cInfo.id)
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
if (i >= 0) {
|
||||
decreaseUnreadCounter(currentUser.value!!, chats[i].chatStats.unreadCount)
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount)
|
||||
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
|
||||
}
|
||||
// clear current chat
|
||||
@ -343,15 +353,15 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCurrentUser(newProfile: Profile, preferences: FullChatPreferences? = null) {
|
||||
fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) {
|
||||
val current = currentUser.value ?: return
|
||||
val updated = current.copy(
|
||||
profile = newProfile.toLocalProfile(current.profile.profileId),
|
||||
fullPreferences = preferences ?: current.fullPreferences
|
||||
)
|
||||
val indexInUsers = users.indexOfFirst { it.user.userId == current.userId }
|
||||
if (indexInUsers != -1) {
|
||||
users[indexInUsers] = UserInfo(updated, users[indexInUsers].unreadCount)
|
||||
val i = users.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId }
|
||||
if (i != -1) {
|
||||
users[i] = users[i].copy(user = updated)
|
||||
}
|
||||
currentUser.value = updated
|
||||
}
|
||||
@ -370,16 +380,17 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatItemsRead(cInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
|
||||
val markedRead = markItemsReadInCurrentChat(cInfo, range)
|
||||
fun markChatItemsRead(chat: Chat, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
|
||||
val cInfo = chat.chatInfo
|
||||
val markedRead = markItemsReadInCurrentChat(chat, range)
|
||||
// update preview
|
||||
val chatIdx = getChatIndex(cInfo.id)
|
||||
val chatIdx = getChatIndex(chat.remoteHostId, cInfo.id)
|
||||
if (chatIdx >= 0) {
|
||||
val chat = chats[chatIdx]
|
||||
val lastId = chat.chatItems.lastOrNull()?.id
|
||||
if (lastId != null) {
|
||||
val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0
|
||||
decreaseUnreadCounter(currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
decreaseUnreadCounter(chat.remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIdx] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCount,
|
||||
@ -391,7 +402,8 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
private fun markItemsReadInCurrentChat(cInfo: ChatInfo, range: CC.ItemRange? = null): Int {
|
||||
private fun markItemsReadInCurrentChat(chat: Chat, range: CC.ItemRange? = null): Int {
|
||||
val cInfo = chat.chatInfo
|
||||
var markedRead = 0
|
||||
if (chatId.value == cInfo.id) {
|
||||
var i = 0
|
||||
@ -415,13 +427,13 @@ object ChatModel {
|
||||
return markedRead
|
||||
}
|
||||
|
||||
private fun decreaseCounterInChat(chatId: ChatId) {
|
||||
val chatIndex = getChatIndex(chatId)
|
||||
private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) {
|
||||
val chatIndex = getChatIndex(rhId, chatId)
|
||||
if (chatIndex == -1) return
|
||||
|
||||
val chat = chats[chatIndex]
|
||||
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
|
||||
decreaseUnreadCounter(currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIndex] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCount,
|
||||
@ -429,18 +441,18 @@ object ChatModel {
|
||||
)
|
||||
}
|
||||
|
||||
fun increaseUnreadCounter(user: UserLike) {
|
||||
changeUnreadCounter(user, 1)
|
||||
fun increaseUnreadCounter(rhId: Long?, user: UserLike) {
|
||||
changeUnreadCounter(rhId, user, 1)
|
||||
}
|
||||
|
||||
fun decreaseUnreadCounter(user: UserLike, by: Int = 1) {
|
||||
changeUnreadCounter(user, -by)
|
||||
fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) {
|
||||
changeUnreadCounter(rhId, user, -by)
|
||||
}
|
||||
|
||||
private fun changeUnreadCounter(user: UserLike, by: Int) {
|
||||
val i = users.indexOfFirst { it.user.userId == user.userId }
|
||||
private fun changeUnreadCounter(rhId: Long?, user: UserLike, by: Int) {
|
||||
val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId }
|
||||
if (i != -1) {
|
||||
users[i] = UserInfo(users[i].user, users[i].unreadCount + by)
|
||||
users[i] = users[i].copy(unreadCount = users[i].unreadCount + by)
|
||||
}
|
||||
}
|
||||
|
||||
@ -536,14 +548,14 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChat(id: String) {
|
||||
chats.removeAll { it.id == id }
|
||||
fun removeChat(rhId: Long?, id: String) {
|
||||
chats.removeAll { it.id == id && it.remoteHostId == rhId }
|
||||
}
|
||||
|
||||
fun upsertGroupMember(groupInfo: GroupInfo, member: GroupMember): Boolean {
|
||||
fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean {
|
||||
// user member was updated
|
||||
if (groupInfo.membership.groupMemberId == member.groupMemberId) {
|
||||
updateGroup(groupInfo)
|
||||
updateGroup(rhId, groupInfo)
|
||||
return false
|
||||
}
|
||||
// update current chat
|
||||
@ -561,12 +573,12 @@ object ChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGroupMemberConnectionStats(groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) {
|
||||
fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) {
|
||||
val memberConn = member.activeConn
|
||||
if (memberConn != null) {
|
||||
val updatedConn = memberConn.copy(connectionStats = connectionStats)
|
||||
val updatedMember = member.copy(activeConn = updatedConn)
|
||||
upsertGroupMember(groupInfo, updatedMember)
|
||||
upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
|
||||
@ -591,6 +603,8 @@ object ChatModel {
|
||||
}
|
||||
terminalItems.add(item)
|
||||
}
|
||||
|
||||
fun connectedToRemote(): Boolean = currentRemoteHost.value != null || remoteCtrlSession.value?.active == true
|
||||
}
|
||||
|
||||
enum class ChatType(val type: String) {
|
||||
@ -602,6 +616,7 @@ enum class ChatType(val type: String) {
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
val remoteHostId: Long? = null,
|
||||
override val userId: Long,
|
||||
val userContactId: Long,
|
||||
val localDisplayName: String,
|
||||
@ -701,9 +716,10 @@ interface SomeChat {
|
||||
|
||||
@Serializable @Stable
|
||||
data class Chat (
|
||||
val remoteHostId: Long? = null,
|
||||
val chatInfo: ChatInfo,
|
||||
val chatItems: List<ChatItem>,
|
||||
val chatStats: ChatStats = ChatStats(),
|
||||
val chatStats: ChatStats = ChatStats()
|
||||
) {
|
||||
val userCanSend: Boolean
|
||||
get() = when (chatInfo) {
|
||||
@ -2219,7 +2235,7 @@ enum class MREmojiChar(val value: String) {
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class CIFile(
|
||||
data class CIFile(
|
||||
val fileId: Long,
|
||||
val fileName: String,
|
||||
val fileSize: Long,
|
||||
@ -2263,6 +2279,39 @@ class CIFile(
|
||||
is CIFileStatus.Invalid -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* DO NOT CALL this function in compose scope, [LaunchedEffect], [DisposableEffect] and so on. Only with [withBGApi] or [runBlocking].
|
||||
* Otherwise, it will be canceled when moving to another screen/item/view, etc
|
||||
* */
|
||||
suspend fun loadRemoteFile(allowToShowAlert: Boolean): Boolean {
|
||||
val rh = chatModel.currentRemoteHost.value
|
||||
val user = chatModel.currentUser.value
|
||||
if (rh == null || user == null || fileSource == null || !loaded) return false
|
||||
if (getLoadedFilePath(this) != null) return true
|
||||
if (cachedRemoteFileRequests.contains(fileSource)) return false
|
||||
|
||||
val rf = RemoteFile(
|
||||
userId = user.userId,
|
||||
fileId = fileId,
|
||||
sent = fileStatus.sent,
|
||||
fileSource = fileSource
|
||||
)
|
||||
cachedRemoteFileRequests.add(fileSource)
|
||||
val showAlert = fileSize > 5_000_000 && allowToShowAlert
|
||||
if (showAlert) {
|
||||
AlertManager.shared.showAlertMsgWithProgress(
|
||||
title = generalGetString(MR.strings.loading_remote_file_title),
|
||||
text = generalGetString(MR.strings.loading_remote_file_desc)
|
||||
)
|
||||
}
|
||||
val res = chatModel.controller.getRemoteFile(rh.remoteHostId, rf)
|
||||
cachedRemoteFileRequests.remove(fileSource)
|
||||
if (showAlert) {
|
||||
AlertManager.shared.hideAlert()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getSample(
|
||||
fileId: Long = 1,
|
||||
@ -2272,6 +2321,8 @@ class CIFile(
|
||||
fileStatus: CIFileStatus = CIFileStatus.RcvComplete
|
||||
): CIFile =
|
||||
CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, fileSource = if (filePath == null) null else CryptoFile.plain(filePath), fileStatus = fileStatus, fileProtocol = FileProtocol.XFTP)
|
||||
|
||||
val cachedRemoteFileRequests = SnapshotStateList<CryptoFile>()
|
||||
}
|
||||
}
|
||||
|
||||
@ -2303,6 +2354,8 @@ data class CryptoFile(
|
||||
|
||||
companion object {
|
||||
fun plain(f: String): CryptoFile = CryptoFile(f, null)
|
||||
|
||||
fun desktopPlain(f: URI): CryptoFile = CryptoFile(URLDecoder.decode(f.rawPath, "UTF-8"), null)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2365,6 +2418,21 @@ sealed class CIFileStatus {
|
||||
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
|
||||
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
|
||||
@Serializable @SerialName("invalid") class Invalid(val text: String): CIFileStatus()
|
||||
|
||||
val sent: Boolean get() = when (this) {
|
||||
is SndStored -> true
|
||||
is SndTransfer -> true
|
||||
is SndComplete -> true
|
||||
is SndCancelled -> true
|
||||
is SndError -> true
|
||||
is RcvInvitation -> false
|
||||
is RcvAccepted -> false
|
||||
is RcvTransfer -> false
|
||||
is RcvComplete -> false
|
||||
is RcvCancelled -> false
|
||||
is RcvError -> false
|
||||
is Invalid -> false
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
|
||||
@ -2843,3 +2911,34 @@ enum class NotificationPreviewMode {
|
||||
val default: NotificationPreviewMode = MESSAGE
|
||||
}
|
||||
}
|
||||
|
||||
data class RemoteCtrlSession(
|
||||
val ctrlAppInfo: CtrlAppInfo,
|
||||
val appVersion: String,
|
||||
val sessionState: UIRemoteCtrlSessionState
|
||||
) {
|
||||
val active: Boolean
|
||||
get () = sessionState is UIRemoteCtrlSessionState.Connected
|
||||
|
||||
val sessionCode: String?
|
||||
get() = when (val s = sessionState) {
|
||||
is UIRemoteCtrlSessionState.PendingConfirmation -> s.sessionCode
|
||||
is UIRemoteCtrlSessionState.Connected -> s.sessionCode
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class RemoteCtrlSessionState {
|
||||
@Serializable @SerialName("starting") object Starting: RemoteCtrlSessionState()
|
||||
@Serializable @SerialName("connecting") object Connecting: RemoteCtrlSessionState()
|
||||
@Serializable @SerialName("pendingConfirmation") data class PendingConfirmation(val sessionCode: String): RemoteCtrlSessionState()
|
||||
@Serializable @SerialName("connected") data class Connected(val sessionCode: String): RemoteCtrlSessionState()
|
||||
}
|
||||
|
||||
sealed class UIRemoteCtrlSessionState {
|
||||
@Serializable @SerialName("starting") object Starting: UIRemoteCtrlSessionState()
|
||||
@Serializable @SerialName("connecting") data class Connecting(val remoteCtrl_: RemoteCtrlInfo? = null): UIRemoteCtrlSessionState()
|
||||
@Serializable @SerialName("pendingConfirmation") data class PendingConfirmation(val remoteCtrl_: RemoteCtrlInfo? = null, val sessionCode: String): UIRemoteCtrlSessionState()
|
||||
@Serializable @SerialName("connected") data class Connected(val remoteCtrl: RemoteCtrlInfo, val sessionCode: String): UIRemoteCtrlSessionState()
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -18,6 +18,8 @@ enum class AppPlatform {
|
||||
|
||||
expect val appPlatform: AppPlatform
|
||||
|
||||
expect val deviceName: String
|
||||
|
||||
val appVersionInfo: Pair<String, Int?> = if (appPlatform == AppPlatform.ANDROID)
|
||||
BuildConfigCommon.ANDROID_VERSION_NAME to BuildConfigCommon.ANDROID_VERSION_CODE
|
||||
else
|
||||
|
@ -15,6 +15,7 @@ external fun pipeStdOutToSocket(socketName: String) : Int
|
||||
typealias ChatCtrl = Long
|
||||
external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array<Any>
|
||||
external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String
|
||||
external fun chatSendRemoteCmd(ctrl: ChatCtrl, rhId: Int, msg: String): String
|
||||
external fun chatRecvMsg(ctrl: ChatCtrl): String
|
||||
external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String
|
||||
external fun chatParseMarkdown(str: String): String
|
||||
@ -52,7 +53,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
||||
} else if (startChat) {
|
||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
||||
val user = chatController.apiGetActiveUser()
|
||||
val user = chatController.apiGetActiveUser(null)
|
||||
if (user == null) {
|
||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||
|
@ -24,6 +24,8 @@ expect val agentDatabaseFileName: String
|
||||
* */
|
||||
expect val databaseExportDir: File
|
||||
|
||||
expect val remoteHostsDir: File
|
||||
|
||||
expect fun desktopOpenDatabaseDir()
|
||||
|
||||
fun copyFileToFile(from: File, to: URI, finally: () -> Unit) {
|
||||
@ -59,14 +61,20 @@ fun copyBytesToFile(bytes: ByteArrayInputStream, to: URI, finally: () -> Unit) {
|
||||
}
|
||||
|
||||
fun getAppFilePath(fileName: String): String {
|
||||
return appFilesDir.absolutePath + File.separator + fileName
|
||||
val rh = chatModel.currentRemoteHost.value
|
||||
val s = File.separator
|
||||
return if (rh == null) {
|
||||
appFilesDir.absolutePath + s + fileName
|
||||
} else {
|
||||
remoteHostsDir.absolutePath + s + rh.storePath + s + "simplex_v1_files" + s + fileName
|
||||
}
|
||||
}
|
||||
|
||||
fun getLoadedFilePath(file: CIFile?): String? {
|
||||
val f = file?.fileSource?.filePath
|
||||
return if (f != null && file.loaded) {
|
||||
val filePath = getAppFilePath(f)
|
||||
if (File(filePath).exists()) filePath else null
|
||||
if (fileReady(file, filePath)) filePath else null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -76,12 +84,17 @@ fun getLoadedFileSource(file: CIFile?): CryptoFile? {
|
||||
val f = file?.fileSource?.filePath
|
||||
return if (f != null && file.loaded) {
|
||||
val filePath = getAppFilePath(f)
|
||||
if (File(filePath).exists()) file.fileSource else null
|
||||
if (fileReady(file, filePath)) file.fileSource else null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun fileReady(file: CIFile, filePath: String) =
|
||||
File(filePath).exists() &&
|
||||
!CIFile.cachedRemoteFileRequests.contains(file.fileSource)
|
||||
&& File(filePath).length() >= file.fileSize
|
||||
|
||||
/**
|
||||
* [rememberedValue] is used in `remember(rememberedValue)`. So when the value changes, file saver will update a callback function
|
||||
* */
|
||||
|
@ -49,7 +49,8 @@ abstract class NtfManager {
|
||||
null
|
||||
}
|
||||
val apiId = chatId.replace("<@", "").toLongOrNull() ?: return
|
||||
acceptContactRequest(incognito, apiId, cInfo, isCurrentUser, ChatModel)
|
||||
// TODO include remote host in notification
|
||||
acceptContactRequest(null, incognito, apiId, cInfo, isCurrentUser, ChatModel)
|
||||
cancelNotificationsForChat(chatId)
|
||||
}
|
||||
|
||||
@ -57,11 +58,12 @@ abstract class NtfManager {
|
||||
withBGApi {
|
||||
awaitChatStartedIfNeeded(chatModel)
|
||||
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
||||
chatModel.controller.changeActiveUser(userId, null)
|
||||
// TODO include remote host ID in desktop notifications?
|
||||
chatModel.controller.changeActiveUser(null, userId, null)
|
||||
}
|
||||
val cInfo = chatModel.getChat(chatId)?.chatInfo
|
||||
chatModel.clearOverlays.value = true
|
||||
if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(cInfo, chatModel)
|
||||
if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(null, cInfo, chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,7 +71,8 @@ abstract class NtfManager {
|
||||
withBGApi {
|
||||
awaitChatStartedIfNeeded(chatModel)
|
||||
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
||||
chatModel.controller.changeActiveUser(userId, null)
|
||||
// TODO include remote host ID in desktop notifications?
|
||||
chatModel.controller.changeActiveUser(null, userId, null)
|
||||
}
|
||||
chatModel.chatId.value = null
|
||||
chatModel.clearOverlays.value = true
|
||||
|
@ -47,13 +47,14 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
|
||||
val s = composeState.value.message
|
||||
if (s.startsWith("/sql") && (!prefPerformLA || !developerTools)) {
|
||||
val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.CommandError("Failed reading: empty")))
|
||||
chatModel.addTerminalItem(TerminalItem.cmd(CC.Console(s)))
|
||||
chatModel.addTerminalItem(TerminalItem.resp(resp))
|
||||
chatModel.addTerminalItem(TerminalItem.cmd(null, CC.Console(s)))
|
||||
chatModel.addTerminalItem(TerminalItem.resp(null, resp))
|
||||
composeState.value = ComposeState(useLinkPreviews = false)
|
||||
} else {
|
||||
withApi {
|
||||
// show "in progress"
|
||||
chatModel.controller.sendCmd(CC.Console(s))
|
||||
// TODO show active remote host in chat console?
|
||||
chatModel.controller.sendCmd(chatModel.remoteHostId, CC.Console(s))
|
||||
composeState.value = ComposeState(useLinkPreviews = false)
|
||||
// hide "in progress"
|
||||
}
|
||||
@ -139,8 +140,10 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
LazyColumn(state = listState, reverseLayout = true) {
|
||||
items(reversedTerminalItems) { item ->
|
||||
val rhId = item.remoteHostId
|
||||
val rhIdStr = if (rhId == null) "" else "$rhId "
|
||||
Text(
|
||||
"${item.date.toString().subSequence(11, 19)} ${item.label}",
|
||||
"$rhIdStr${item.date.toString().subSequence(11, 19)} ${item.label}",
|
||||
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 18.sp, color = MaterialTheme.colors.primary),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
@ -170,18 +170,19 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) {
|
||||
|
||||
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () -> Unit) {
|
||||
withApi {
|
||||
val rhId = chatModel.remoteHostId
|
||||
val user = chatModel.controller.apiCreateActiveUser(
|
||||
Profile(displayName.trim(), "", null)
|
||||
rhId, Profile(displayName.trim(), "", null)
|
||||
) ?: return@withApi
|
||||
chatModel.currentUser.value = user
|
||||
if (chatModel.users.isEmpty()) {
|
||||
chatModel.controller.startChat(user)
|
||||
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
|
||||
} else {
|
||||
val users = chatModel.controller.listUsers()
|
||||
val users = chatModel.controller.listUsers(rhId)
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
chatModel.controller.getUserChatData()
|
||||
chatModel.controller.getUserChatData(rhId)
|
||||
close()
|
||||
}
|
||||
}
|
||||
@ -190,7 +191,7 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: ()
|
||||
fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () -> Unit) {
|
||||
withApi {
|
||||
chatModel.controller.apiCreateActiveUser(
|
||||
Profile(displayName.trim(), "", null)
|
||||
null, Profile(displayName.trim(), "", null)
|
||||
) ?: return@withApi
|
||||
val onboardingStage = chatModel.controller.appPrefs.onboardingStage
|
||||
if (chatModel.users.isEmpty()) {
|
||||
|
@ -43,6 +43,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||
private fun justAcceptIncomingCall(invitation: RcvCallInvitation) {
|
||||
with (chatModel) {
|
||||
activeCall.value = Call(
|
||||
remoteHostId = invitation.remoteHostId,
|
||||
contact = invitation.contact,
|
||||
callState = CallState.InvitationAccepted,
|
||||
localMedia = invitation.callType.media,
|
||||
@ -76,7 +77,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||
Log.d(TAG, "CallManager.endCall: ending call...")
|
||||
callCommand.add(WCallCommand.End)
|
||||
showCallView.value = false
|
||||
controller.apiEndCall(call.contact)
|
||||
controller.apiEndCall(call.remoteHostId, call.contact)
|
||||
activeCall.value = null
|
||||
}
|
||||
}
|
||||
@ -90,7 +91,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||
ntfManager.cancelCallNotification()
|
||||
}
|
||||
withApi {
|
||||
if (!controller.apiRejectCall(invitation.contact)) {
|
||||
if (!controller.apiRejectCall(invitation.remoteHostId, invitation.contact)) {
|
||||
Log.e(TAG, "apiRejectCall error")
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
data class Call(
|
||||
val remoteHostId: Long? = null,
|
||||
val contact: Contact,
|
||||
val callState: CallState,
|
||||
val localMedia: CallMediaType,
|
||||
@ -95,7 +96,14 @@ sealed class WCallResponse {
|
||||
@Serializable data class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
|
||||
@Serializable data class WebRTCExtraInfo(val rtcIceCandidates: String)
|
||||
@Serializable data class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
|
||||
@Serializable data class RcvCallInvitation(val user: User, val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
|
||||
@Serializable data class RcvCallInvitation(
|
||||
val remoteHostId: Long? = null,
|
||||
val user: User,
|
||||
val contact: Contact,
|
||||
val callType: CallType,
|
||||
val sharedKey: String? = null,
|
||||
val callTs: Instant
|
||||
) {
|
||||
val callTypeText: String get() = generalGetString(when(callType.media) {
|
||||
CallMediaType.Video -> if (sharedKey == null) MR.strings.video_call_no_encryption else MR.strings.encrypted_video_call
|
||||
CallMediaType.Audio -> if (sharedKey == null) MR.strings.audio_call_no_encryption else MR.strings.encrypted_audio_call
|
||||
|
@ -61,6 +61,7 @@ fun ChatInfoView(
|
||||
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) {
|
||||
mutableStateOf(chatModel.contactNetworkStatus(contact))
|
||||
}
|
||||
val chatRh = chat.remoteHostId
|
||||
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
|
||||
ChatInfoLayout(
|
||||
chat,
|
||||
@ -81,25 +82,25 @@ fun ChatInfoView(
|
||||
connectionCode,
|
||||
developerTools,
|
||||
onLocalAliasChanged = {
|
||||
setContactAlias(chat.chatInfo.apiId, it, chatModel)
|
||||
setContactAlias(chat, it, chatModel)
|
||||
},
|
||||
openPreferences = {
|
||||
ModalManager.end.showCustomModal { close ->
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
ContactPreferencesView(chatModel, user, contact.contactId, close)
|
||||
ContactPreferencesView(chatModel, user, chatRh, contact.contactId, close)
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },
|
||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||
deleteContact = { deleteContactDialog(chat, chatModel, close) },
|
||||
clearChat = { clearChatDialog(chat, chatModel, close) },
|
||||
switchContactAddress = {
|
||||
showSwitchAddressAlert(switchAddress = {
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiSwitchContact(contact.contactId)
|
||||
val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
@ -108,20 +109,20 @@ fun ChatInfoView(
|
||||
abortSwitchContactAddress = {
|
||||
showAbortSwitchAddressAlert(abortSwitchAddress = {
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiAbortSwitchContact(contact.contactId)
|
||||
val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
syncContactConnection = {
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = false)
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
@ -129,10 +130,10 @@ fun ChatInfoView(
|
||||
syncContactConnectionForce = {
|
||||
showSyncConnectionForceAlert(syncConnectionForce = {
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = true)
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true)
|
||||
connStats.value = cStats
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
close.invoke()
|
||||
}
|
||||
@ -146,9 +147,10 @@ fun ChatInfoView(
|
||||
connectionCode,
|
||||
ct.verified,
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyContact(ct.contactId, code)?.let { r ->
|
||||
chatModel.controller.apiVerifyContact(chatRh, ct.contactId, code)?.let { r ->
|
||||
val (verified, existingCode) = r
|
||||
chatModel.updateContact(
|
||||
chatRh,
|
||||
ct.copy(
|
||||
activeConn = ct.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
@ -195,7 +197,8 @@ sealed class SendReceipts {
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val chatInfo = chat.chatInfo
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.delete_contact_question),
|
||||
text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)),
|
||||
@ -206,7 +209,7 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
withApi {
|
||||
deleteContact(chatInfo, chatModel, close, notify = true)
|
||||
deleteContact(chat, chatModel, close, notify = true)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
@ -215,7 +218,7 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
withApi {
|
||||
deleteContact(chatInfo, chatModel, close, notify = false)
|
||||
deleteContact(chat, chatModel, close, notify = false)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
@ -225,7 +228,7 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
withApi {
|
||||
deleteContact(chatInfo, chatModel, close)
|
||||
deleteContact(chat, chatModel, close)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
@ -242,11 +245,13 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() ->
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteContact(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) {
|
||||
fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) {
|
||||
val chatInfo = chat.chatInfo
|
||||
withApi {
|
||||
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId, notify)
|
||||
val chatRh = chat.remoteHostId
|
||||
val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify)
|
||||
if (r) {
|
||||
chatModel.removeChat(chatInfo.id)
|
||||
chatModel.removeChat(chatRh, chatInfo.id)
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
@ -257,16 +262,18 @@ fun deleteContact(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)?
|
||||
}
|
||||
}
|
||||
|
||||
fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
fun clearChatDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val chatInfo = chat.chatInfo
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.clear_chat_question),
|
||||
text = generalGetString(MR.strings.clear_chat_warning),
|
||||
confirmText = generalGetString(MR.strings.clear_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val updatedChatInfo = chatModel.controller.apiClearChat(chatInfo.chatType, chatInfo.apiId)
|
||||
val chatRh = chat.remoteHostId
|
||||
val updatedChatInfo = chatModel.controller.apiClearChat(chatRh, chatInfo.chatType, chatInfo.apiId)
|
||||
if (updatedChatInfo != null) {
|
||||
chatModel.clearChat(updatedChatInfo)
|
||||
chatModel.clearChat(chatRh, updatedChatInfo)
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
close?.invoke()
|
||||
}
|
||||
@ -669,9 +676,10 @@ fun ShareAddressButton(onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel) = withApi {
|
||||
chatModel.controller.apiSetContactAlias(contactApiId, localAlias)?.let {
|
||||
chatModel.updateContact(it)
|
||||
private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withApi {
|
||||
val chatRh = chat.remoteHostId
|
||||
chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let {
|
||||
chatModel.updateContact(chatRh, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,11 +100,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
}
|
||||
}
|
||||
val view = LocalMultiplatformView()
|
||||
if (activeChat.value == null || user == null) {
|
||||
val chat = activeChat.value
|
||||
if (chat == null || user == null) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
} else {
|
||||
val chat = activeChat.value!!
|
||||
val chatRh = chat.remoteHostId
|
||||
// We need to have real unreadCount value for displaying it inside top right button
|
||||
// Having activeChat reloaded on every change in it is inefficient (UI lags)
|
||||
val unreadCount = remember {
|
||||
@ -166,11 +167,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
var preloadedCode: String? = null
|
||||
var preloadedLink: Pair<String, GroupMemberRole>? = null
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
preloadedContactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
|
||||
preloadedCode = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)?.second
|
||||
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
|
||||
preloadedLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
|
||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||
}
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
val chat = remember { activeChat }.value
|
||||
@ -178,20 +179,20 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
||||
var code: String? by remember { mutableStateOf(preloadedCode) }
|
||||
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
|
||||
contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
|
||||
contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
preloadedContactInfo = contactInfo
|
||||
code = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)?.second
|
||||
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||
preloadedCode = code
|
||||
}
|
||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||
KeyChangeEffect(chat.id) {
|
||||
setGroupMembers((chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
|
||||
link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
|
||||
setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
|
||||
link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||
preloadedLink = link
|
||||
}
|
||||
GroupChatInfoView(chatModel, link?.first, link?.second, {
|
||||
GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, {
|
||||
link = it
|
||||
preloadedLink = it
|
||||
}, close)
|
||||
@ -202,19 +203,19 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val (_, code) = if (member.memberActive) {
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId)
|
||||
member to memCode?.second
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, close, close)
|
||||
GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -225,7 +226,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
if (c != null && firstId != null) {
|
||||
withApi {
|
||||
Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}")
|
||||
apiLoadPrevMessages(c.chatInfo, chatModel, firstId, searchText.value)
|
||||
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
|
||||
Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}")
|
||||
}
|
||||
}
|
||||
@ -241,6 +242,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
val toChatItem: ChatItem?
|
||||
if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
|
||||
val r = chatModel.controller.apiDeleteMemberChatItem(
|
||||
chatRh,
|
||||
groupId = groupInfo.groupId,
|
||||
groupMemberId = groupMember.groupMemberId,
|
||||
itemId = itemId
|
||||
@ -249,6 +251,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
toChatItem = r?.second
|
||||
} else {
|
||||
val r = chatModel.controller.apiDeleteChatItem(
|
||||
chatRh,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = itemId,
|
||||
@ -258,9 +261,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
toChatItem = r?.toChatItem?.chatItem
|
||||
}
|
||||
if (toChatItem == null && deletedChatItem != null) {
|
||||
chatModel.removeChatItem(cInfo, deletedChatItem)
|
||||
chatModel.removeChatItem(chatRh, cInfo, deletedChatItem)
|
||||
} else if (toChatItem != null) {
|
||||
chatModel.upsertChatItem(cInfo, toChatItem)
|
||||
chatModel.upsertChatItem(chatRh, cInfo, toChatItem)
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -271,27 +274,27 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
val deletedItems: ArrayList<ChatItem> = arrayListOf()
|
||||
for (itemId in itemIds) {
|
||||
val di = chatModel.controller.apiDeleteChatItem(
|
||||
chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
|
||||
chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
|
||||
)?.deletedChatItem?.chatItem
|
||||
if (di != null) {
|
||||
deletedItems.add(di)
|
||||
}
|
||||
}
|
||||
for (di in deletedItems) {
|
||||
chatModel.removeChatItem(chatInfo, di)
|
||||
chatModel.removeChatItem(chatRh, chatInfo, di)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
receiveFile = { fileId, encrypted ->
|
||||
withApi { chatModel.controller.receiveFile(user, fileId, encrypted) }
|
||||
withApi { chatModel.controller.receiveFile(chatRh, user, fileId, encrypted) }
|
||||
},
|
||||
cancelFile = { fileId ->
|
||||
withApi { chatModel.controller.cancelFile(user, fileId) }
|
||||
withApi { chatModel.controller.cancelFile(chatRh, user, fileId) }
|
||||
},
|
||||
joinGroup = { groupId, onComplete ->
|
||||
withApi {
|
||||
chatModel.controller.apiJoinGroup(groupId)
|
||||
chatModel.controller.apiJoinGroup(chatRh, groupId)
|
||||
onComplete.invoke()
|
||||
}
|
||||
},
|
||||
@ -299,7 +302,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
chatModel.activeCall.value = Call(contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media)
|
||||
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
@ -320,48 +323,48 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
},
|
||||
acceptFeature = { contact, feature, param ->
|
||||
withApi {
|
||||
chatModel.controller.allowFeatureToContact(contact, feature, param)
|
||||
chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param)
|
||||
}
|
||||
},
|
||||
openDirectChat = { contactId ->
|
||||
withApi {
|
||||
openDirectChat(contactId, chatModel)
|
||||
openDirectChat(chatRh, contactId, chatModel)
|
||||
}
|
||||
},
|
||||
updateContactStats = { contact ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
|
||||
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||
if (r != null) {
|
||||
val contactStats = r.first
|
||||
if (contactStats != null)
|
||||
chatModel.updateContactConnectionStats(contact, contactStats)
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, contactStats)
|
||||
}
|
||||
}
|
||||
},
|
||||
updateMemberStats = { groupInfo, member ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
val memStats = r.second
|
||||
if (memStats != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, memStats)
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
syncContactConnection = { contact ->
|
||||
withApi {
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = false)
|
||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
|
||||
if (cStats != null) {
|
||||
chatModel.updateContactConnectionStats(contact, cStats)
|
||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||
}
|
||||
}
|
||||
},
|
||||
syncMemberConnection = { groupInfo, member ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = false)
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -374,6 +377,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
setReaction = { cInfo, cItem, add, reaction ->
|
||||
withApi {
|
||||
val updatedCI = chatModel.controller.apiChatItemReaction(
|
||||
rh = chatRh,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = cItem.id,
|
||||
@ -387,10 +391,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
},
|
||||
showItemDetails = { cInfo, cItem ->
|
||||
withApi {
|
||||
val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id)
|
||||
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
|
||||
if (ciInfo != null) {
|
||||
if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
|
||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||
}
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModal(endButtons = { ShareButton {
|
||||
@ -404,28 +408,29 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
addMembers = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(groupInfo, false, chatModel, close)
|
||||
AddGroupMembersView(chatRh, groupInfo, false, chatModel, close)
|
||||
}
|
||||
}
|
||||
},
|
||||
openGroupLink = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
val link = chatModel.controller.apiGetGroupLink(groupInfo.groupId)
|
||||
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.end.showModalCloseable(true) {
|
||||
GroupLinkView(chatModel, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
|
||||
GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
|
||||
}
|
||||
}
|
||||
},
|
||||
markRead = { range, unreadCountAfter ->
|
||||
chatModel.markChatItemsRead(chat.chatInfo, range, unreadCountAfter)
|
||||
chatModel.markChatItemsRead(chat, range, unreadCountAfter)
|
||||
ntfManager.cancelNotificationsForChat(chat.id)
|
||||
withBGApi {
|
||||
chatModel.controller.apiChatRead(
|
||||
chatRh,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
range
|
||||
@ -437,7 +442,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
if (searchText.value == value) return@ChatLayout
|
||||
val c = chatModel.getChat(chat.chatInfo.id) ?: return@ChatLayout
|
||||
withApi {
|
||||
apiFindMessages(c.chatInfo, chatModel, value)
|
||||
apiFindMessages(c, chatModel, value)
|
||||
searchText.value = value
|
||||
}
|
||||
},
|
||||
@ -498,6 +503,7 @@ fun ChatLayout(
|
||||
enabled = !attachmentDisabled.value && rememberUpdatedState(chat.userCanSend).value,
|
||||
onFiles = { paths -> composeState.onFilesAttached(paths.map { URI.create(it) }) },
|
||||
onImage = {
|
||||
// TODO: file is not saved anywhere?!
|
||||
val tmpFile = File.createTempFile("image", ".bmp", tmpDir)
|
||||
tmpFile.deleteOnExit()
|
||||
chatModel.filesToDelete.add(tmpFile)
|
||||
@ -1252,14 +1258,16 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha
|
||||
val chat = activeChat.value
|
||||
if (chat?.chatStats?.unreadChat != true) return
|
||||
withApi {
|
||||
val chatRh = chat.remoteHostId
|
||||
val success = chatModel.controller.apiChatUnread(
|
||||
chatRh,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
false
|
||||
)
|
||||
if (success && chat.id == activeChat.value?.id) {
|
||||
activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))
|
||||
chatModel.replaceChat(chat.id, activeChat.value!!)
|
||||
chatModel.replaceChat(chatRh, chat.id, activeChat.value!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1299,7 +1307,7 @@ private fun providerForGallery(
|
||||
scrollTo: (Int) -> Unit
|
||||
): ImageGalleryProvider {
|
||||
fun canShowMedia(item: ChatItem): Boolean =
|
||||
(item.content.msgContent is MsgContent.MCImage || item.content.msgContent is MsgContent.MCVideo) && (item.file?.loaded == true && getLoadedFilePath(item.file) != null)
|
||||
(item.content.msgContent is MsgContent.MCImage || item.content.msgContent is MsgContent.MCVideo) && (item.file?.loaded == true && (getLoadedFilePath(item.file) != null || chatModel.connectedToRemote()))
|
||||
|
||||
fun item(skipInternalIndex: Int, initialChatId: Long): Pair<Int, ChatItem>? {
|
||||
var processedInternalIndex = -skipInternalIndex.sign
|
||||
@ -1326,7 +1334,7 @@ private fun providerForGallery(
|
||||
val item = item(internalIndex, initialChatId)?.second ?: return null
|
||||
return when (item.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
val res = getLoadedImage(item.file)
|
||||
val res = runBlocking { getLoadedImage(item.file) }
|
||||
val filePath = getLoadedFilePath(item.file)
|
||||
if (res != null && filePath != null) {
|
||||
val (imageBitmap: ImageBitmap, data: ByteArray) = res
|
||||
@ -1334,7 +1342,7 @@ private fun providerForGallery(
|
||||
} else null
|
||||
}
|
||||
is MsgContent.MCVideo -> {
|
||||
val filePath = getLoadedFilePath(item.file)
|
||||
val filePath = if (chatModel.connectedToRemote() && item.file?.loaded == true) getAppFilePath(item.file.fileName) else getLoadedFilePath(item.file)
|
||||
if (filePath != null) {
|
||||
val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
|
||||
ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image)
|
||||
|
@ -15,6 +15,8 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.filesToDelete
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.Indigo
|
||||
import chat.simplex.common.ui.theme.isSystemInDarkTheme
|
||||
@ -349,8 +351,10 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {
|
||||
suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {
|
||||
val cInfo = chat.chatInfo
|
||||
val aChatItem = chatModel.controller.apiSendMessage(
|
||||
rh = chat.remoteHostId,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
file = file,
|
||||
@ -360,7 +364,7 @@ fun ComposeView(
|
||||
ttl = ttl
|
||||
)
|
||||
if (aChatItem != null) {
|
||||
chatModel.addChatItem(cInfo, aChatItem.chatItem)
|
||||
chatModel.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
|
||||
return aChatItem.chatItem
|
||||
}
|
||||
if (file != null) removeFile(file.filePath)
|
||||
@ -407,23 +411,25 @@ fun ComposeView(
|
||||
|
||||
suspend fun sendMemberContactInvitation() {
|
||||
val mc = checkLinkPreview()
|
||||
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.chatInfo.apiId, mc)
|
||||
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc)
|
||||
if (contact != null) {
|
||||
chatModel.updateContact(contact)
|
||||
chatModel.updateContact(chat.remoteHostId, contact)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateMessage(ei: ChatItem, cInfo: ChatInfo, live: Boolean): ChatItem? {
|
||||
suspend fun updateMessage(ei: ChatItem, chat: Chat, live: Boolean): ChatItem? {
|
||||
val cInfo = chat.chatInfo
|
||||
val oldMsgContent = ei.content.msgContent
|
||||
if (oldMsgContent != null) {
|
||||
val updatedItem = chatModel.controller.apiUpdateChatItem(
|
||||
rh = chat.remoteHostId,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = ei.meta.itemId,
|
||||
mc = updateMsgContent(oldMsgContent),
|
||||
live = live
|
||||
)
|
||||
if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem)
|
||||
if (updatedItem != null) chatModel.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
|
||||
return updatedItem?.chatItem
|
||||
}
|
||||
return null
|
||||
@ -441,21 +447,29 @@ fun ComposeView(
|
||||
sent = null
|
||||
} else if (cs.contextItem is ComposeContextItem.EditingItem) {
|
||||
val ei = cs.contextItem.chatItem
|
||||
sent = updateMessage(ei, cInfo, live)
|
||||
sent = updateMessage(ei, chat, live)
|
||||
} else if (liveMessage != null && liveMessage.sent) {
|
||||
sent = updateMessage(liveMessage.chatItem, cInfo, live)
|
||||
sent = updateMessage(liveMessage.chatItem, chat, live)
|
||||
} else {
|
||||
val msgs: ArrayList<MsgContent> = ArrayList()
|
||||
val files: ArrayList<CryptoFile> = ArrayList()
|
||||
val remoteHost = chatModel.currentRemoteHost.value
|
||||
when (val preview = cs.preview) {
|
||||
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
|
||||
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
|
||||
is ComposePreview.MediaPreview -> {
|
||||
preview.content.forEachIndexed { index, it ->
|
||||
val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()
|
||||
val file = when (it) {
|
||||
is UploadContent.SimpleImage -> saveImage(it.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get())
|
||||
is UploadContent.AnimatedImage -> saveAnimImage(it.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get())
|
||||
is UploadContent.Video -> saveFileFromUri(it.uri, encrypted = false)
|
||||
is UploadContent.SimpleImage ->
|
||||
if (remoteHost == null) saveImage(it.uri, encrypted = encrypted)
|
||||
else desktopSaveImageInTmp(it.uri)
|
||||
is UploadContent.AnimatedImage ->
|
||||
if (remoteHost == null) saveAnimImage(it.uri, encrypted = encrypted)
|
||||
else CryptoFile.desktopPlain(it.uri)
|
||||
is UploadContent.Video ->
|
||||
if (remoteHost == null) saveFileFromUri(it.uri, encrypted = false)
|
||||
else CryptoFile.desktopPlain(it.uri)
|
||||
}
|
||||
if (file != null) {
|
||||
files.add(file)
|
||||
@ -470,22 +484,32 @@ fun ComposeView(
|
||||
is ComposePreview.VoicePreview -> {
|
||||
val tmpFile = File(preview.voice)
|
||||
AudioPlayer.stop(tmpFile.absolutePath)
|
||||
val actualFile = File(getAppFilePath(tmpFile.name.replaceAfter(RecorderInterface.extension, "")))
|
||||
files.add(withContext(Dispatchers.IO) {
|
||||
if (chatController.appPrefs.privacyEncryptLocalFiles.get()) {
|
||||
val args = encryptCryptoFile(tmpFile.absolutePath, actualFile.absolutePath)
|
||||
tmpFile.delete()
|
||||
CryptoFile(actualFile.name, args)
|
||||
} else {
|
||||
Files.move(tmpFile.toPath(), actualFile.toPath())
|
||||
CryptoFile.plain(actualFile.name)
|
||||
}
|
||||
})
|
||||
deleteUnusedFiles()
|
||||
if (remoteHost == null) {
|
||||
val actualFile = File(getAppFilePath(tmpFile.name.replaceAfter(RecorderInterface.extension, "")))
|
||||
files.add(withContext(Dispatchers.IO) {
|
||||
if (chatController.appPrefs.privacyEncryptLocalFiles.get()) {
|
||||
val args = encryptCryptoFile(tmpFile.absolutePath, actualFile.absolutePath)
|
||||
tmpFile.delete()
|
||||
CryptoFile(actualFile.name, args)
|
||||
} else {
|
||||
Files.move(tmpFile.toPath(), actualFile.toPath())
|
||||
CryptoFile.plain(actualFile.name)
|
||||
}
|
||||
})
|
||||
deleteUnusedFiles()
|
||||
} else {
|
||||
files.add(CryptoFile.plain(tmpFile.absolutePath))
|
||||
// It will be deleted on JVM shutdown or next start (if the app crashes unexpectedly)
|
||||
filesToDelete.remove(tmpFile)
|
||||
}
|
||||
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000))
|
||||
}
|
||||
is ComposePreview.FilePreview -> {
|
||||
val file = saveFileFromUri(preview.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get())
|
||||
val file = if (remoteHost == null) {
|
||||
saveFileFromUri(preview.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get())
|
||||
} else {
|
||||
CryptoFile.desktopPlain(preview.uri)
|
||||
}
|
||||
if (file != null) {
|
||||
files.add((file))
|
||||
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
|
||||
@ -499,7 +523,15 @@ fun ComposeView(
|
||||
sent = null
|
||||
msgs.forEachIndexed { index, content ->
|
||||
if (index > 0) delay(100)
|
||||
sent = send(cInfo, content, if (index == 0) quotedItemId else null, files.getOrNull(index),
|
||||
var file = files.getOrNull(index)
|
||||
if (remoteHost != null && file != null) {
|
||||
file = controller.storeRemoteFile(
|
||||
rhId = remoteHost.remoteHostId,
|
||||
storeEncrypted = if (content is MsgContent.MCVideo) false else null,
|
||||
localPath = file.filePath
|
||||
)
|
||||
}
|
||||
sent = send(chat, content, if (index == 0) quotedItemId else null, file,
|
||||
live = if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false,
|
||||
ttl = ttl
|
||||
)
|
||||
@ -509,7 +541,7 @@ fun ComposeView(
|
||||
cs.preview is ComposePreview.FilePreview ||
|
||||
cs.preview is ComposePreview.VoicePreview)
|
||||
) {
|
||||
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
|
||||
sent = send(chat, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
|
||||
}
|
||||
}
|
||||
clearState(live)
|
||||
@ -544,7 +576,7 @@ fun ComposeView(
|
||||
fun allowVoiceToContact() {
|
||||
val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return
|
||||
withApi {
|
||||
chatModel.controller.allowFeatureToContact(contact, ChatFeature.Voice)
|
||||
chatModel.controller.allowFeatureToContact(chat.remoteHostId, contact, ChatFeature.Voice)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ import chat.simplex.res.MR
|
||||
fun ContactPreferencesView(
|
||||
m: ChatModel,
|
||||
user: User,
|
||||
rhId: Long?,
|
||||
contactId: Long,
|
||||
close: () -> Unit,
|
||||
) {
|
||||
@ -36,9 +37,9 @@ fun ContactPreferencesView(
|
||||
fun savePrefs(afterSave: () -> Unit = {}) {
|
||||
withApi {
|
||||
val prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
|
||||
val toContact = m.controller.apiSetContactPrefs(ct.contactId, prefs)
|
||||
val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs)
|
||||
if (toContact != null) {
|
||||
m.updateContact(toContact)
|
||||
m.updateContact(rhId, toContact)
|
||||
currentFeaturesAllowed = featuresAllowed
|
||||
}
|
||||
afterSave()
|
||||
|
@ -11,10 +11,7 @@ import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
expect fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit)
|
||||
|
||||
@Composable
|
||||
fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
|
@ -33,7 +33,7 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) {
|
||||
fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) {
|
||||
val selectedContacts = remember { mutableStateListOf<Long>() }
|
||||
val selectedRole = remember { mutableStateOf(GroupMemberRole.Member) }
|
||||
var allowModifyMembers by remember { mutableStateOf(true) }
|
||||
@ -49,16 +49,16 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
|
||||
searchText,
|
||||
openPreferences = {
|
||||
ModalManager.end.showCustomModal { close ->
|
||||
GroupPreferencesView(chatModel, groupInfo.id, close)
|
||||
GroupPreferencesView(chatModel, rhId, groupInfo.id, close)
|
||||
}
|
||||
},
|
||||
inviteMembers = {
|
||||
allowModifyMembers = false
|
||||
withApi {
|
||||
for (contactId in selectedContacts) {
|
||||
val member = chatModel.controller.apiAddMember(groupInfo.groupId, contactId, selectedRole.value)
|
||||
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
|
||||
if (member != null) {
|
||||
chatModel.upsertGroupMember(groupInfo, member)
|
||||
chatModel.upsertGroupMember(rhId, groupInfo, member)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
@ -41,9 +41,10 @@ import kotlinx.coroutines.launch
|
||||
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit) {
|
||||
fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
// TODO derivedStateOf?
|
||||
val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
|
||||
val currentUser = chatModel.currentUser.value
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) {
|
||||
@ -68,25 +69,25 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
|
||||
groupLink,
|
||||
addMembers = {
|
||||
withApi {
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(groupInfo, false, chatModel, close)
|
||||
AddGroupMembersView(rhId, groupInfo, false, chatModel, close)
|
||||
}
|
||||
}
|
||||
},
|
||||
showMemberInfo = { member ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val (_, code) = if (member.memberActive) {
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(rhId, groupInfo.apiId, member.groupMemberId)
|
||||
member to memCode?.second
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
ModalManager.end.showModalCloseable(true) { closeCurrent ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, closeCurrent) {
|
||||
GroupMemberInfoView(rhId, groupInfo, mem, stats, code, chatModel, closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
}
|
||||
@ -95,31 +96,33 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
|
||||
}
|
||||
},
|
||||
editGroupProfile = {
|
||||
ModalManager.end.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) }
|
||||
ModalManager.end.showCustomModal { close -> GroupProfileView(rhId, groupInfo, chatModel, close) }
|
||||
},
|
||||
addOrEditWelcomeMessage = {
|
||||
ModalManager.end.showCustomModal { close -> GroupWelcomeView(chatModel, groupInfo, close) }
|
||||
ModalManager.end.showCustomModal { close -> GroupWelcomeView(chatModel, rhId, groupInfo, close) }
|
||||
},
|
||||
openPreferences = {
|
||||
ModalManager.end.showCustomModal { close ->
|
||||
GroupPreferencesView(
|
||||
chatModel,
|
||||
rhId,
|
||||
chat.id,
|
||||
close
|
||||
)
|
||||
}
|
||||
},
|
||||
deleteGroup = { deleteGroupDialog(chat.chatInfo, groupInfo, chatModel, close) },
|
||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
|
||||
deleteGroup = { deleteGroupDialog(chat, groupInfo, chatModel, close) },
|
||||
clearChat = { clearChatDialog(chat, chatModel, close) },
|
||||
leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) },
|
||||
manageGroupLink = {
|
||||
ModalManager.end.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
|
||||
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
val chatInfo = chat.chatInfo
|
||||
val alertTextKey =
|
||||
if (groupInfo.membership.memberCurrent) MR.strings.delete_group_for_all_members_cannot_undo_warning
|
||||
else MR.strings.delete_group_for_self_cannot_undo_warning
|
||||
@ -129,9 +132,9 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM
|
||||
confirmText = generalGetString(MR.strings.delete_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
|
||||
val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(chatInfo.id)
|
||||
chatModel.removeChat(chat.remoteHostId, chatInfo.id)
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
@ -145,14 +148,14 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM
|
||||
)
|
||||
}
|
||||
|
||||
fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.leave_group_question),
|
||||
text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
|
||||
confirmText = generalGetString(MR.strings.leave_group_button),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
chatModel.controller.leaveGroup(groupInfo.groupId)
|
||||
chatModel.controller.leaveGroup(rhId, groupInfo.groupId)
|
||||
close?.invoke()
|
||||
}
|
||||
},
|
||||
@ -160,16 +163,16 @@ fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> U
|
||||
)
|
||||
}
|
||||
|
||||
private fun removeMemberAlert(groupInfo: GroupInfo, mem: GroupMember) {
|
||||
private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMember) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.button_remove_member_question),
|
||||
text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone),
|
||||
confirmText = generalGetString(MR.strings.remove_member_confirmation),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val updatedMember = chatModel.controller.apiRemoveMember(groupInfo.groupId, mem.groupMemberId)
|
||||
val updatedMember = chatModel.controller.apiRemoveMember(rhId, groupInfo.groupId, mem.groupMemberId)
|
||||
if (updatedMember != null) {
|
||||
chatModel.upsertGroupMember(groupInfo, updatedMember)
|
||||
chatModel.upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -260,7 +263,7 @@ fun GroupChatInfoLayout(
|
||||
Divider()
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
SectionItemViewLongClickable({ showMemberInfo(member) }, { showMenu.value = true }, minHeight = 54.dp) {
|
||||
DropDownMenuForMember(member, groupInfo, showMenu)
|
||||
DropDownMenuForMember(chat.remoteHostId, member, groupInfo, showMenu)
|
||||
MemberRow(member, onClick = { showMemberInfo(member) })
|
||||
}
|
||||
}
|
||||
@ -413,22 +416,22 @@ private fun MemberVerifiedShield() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DropDownMenuForMember(member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
|
||||
private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (member.canBeRemoved(groupInfo)) {
|
||||
ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
|
||||
removeMemberAlert(groupInfo, member)
|
||||
removeMemberAlert(rhId, groupInfo, member)
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
if (member.memberSettings.showMessages) {
|
||||
ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
|
||||
blockMemberAlert(groupInfo, member)
|
||||
blockMemberAlert(rhId, groupInfo, member)
|
||||
showMenu.value = false
|
||||
})
|
||||
} else {
|
||||
ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = {
|
||||
unblockMemberAlert(groupInfo, member)
|
||||
unblockMemberAlert(rhId, groupInfo, member)
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import chat.simplex.res.MR
|
||||
@Composable
|
||||
fun GroupLinkView(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
groupInfo: GroupInfo,
|
||||
connReqContact: String?,
|
||||
memberRole: GroupMemberRole?,
|
||||
@ -38,7 +39,7 @@ fun GroupLinkView(
|
||||
fun createLink() {
|
||||
creatingLink = true
|
||||
withApi {
|
||||
val link = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
|
||||
val link = chatModel.controller.apiCreateGroupLink(rhId, groupInfo.groupId)
|
||||
if (link != null) {
|
||||
groupLink = link.first
|
||||
groupLinkMemberRole.value = link.second
|
||||
@ -62,7 +63,7 @@ fun GroupLinkView(
|
||||
val role = groupLinkMemberRole.value
|
||||
if (role != null) {
|
||||
withBGApi {
|
||||
val link = chatModel.controller.apiGroupLinkMemberRole(groupInfo.groupId, role)
|
||||
val link = chatModel.controller.apiGroupLinkMemberRole(rhId, groupInfo.groupId, role)
|
||||
if (link != null) {
|
||||
groupLink = link.first
|
||||
groupLinkMemberRole.value = link.second
|
||||
@ -78,7 +79,7 @@ fun GroupLinkView(
|
||||
confirmText = generalGetString(MR.strings.delete_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
|
||||
val r = chatModel.controller.apiDeleteGroupLink(rhId, groupInfo.groupId)
|
||||
if (r) {
|
||||
groupLink = null
|
||||
onGroupLinkUpdated?.invoke(null)
|
||||
|
@ -40,6 +40,7 @@ import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun GroupMemberInfoView(
|
||||
rhId: Long?,
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember,
|
||||
connectionStats: ConnectionStats?,
|
||||
@ -49,7 +50,7 @@ fun GroupMemberInfoView(
|
||||
closeAll: () -> Unit, // Close all open windows up to ChatView
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId }
|
||||
val connStats = remember { mutableStateOf(connectionStats) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
var progressIndicator by remember { mutableStateOf(false) }
|
||||
@ -66,7 +67,7 @@ fun GroupMemberInfoView(
|
||||
getContactChat = { chatModel.getContactChat(it) },
|
||||
openDirectChat = {
|
||||
withApi {
|
||||
val c = chatModel.controller.apiGetChat(ChatType.Direct, it)
|
||||
val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it)
|
||||
if (c != null) {
|
||||
if (chatModel.getContactChat(it) == null) {
|
||||
chatModel.addChat(c)
|
||||
@ -82,9 +83,9 @@ fun GroupMemberInfoView(
|
||||
createMemberContact = {
|
||||
withApi {
|
||||
progressIndicator = true
|
||||
val memberContact = chatModel.controller.apiCreateMemberContact(groupInfo.apiId, member.groupMemberId)
|
||||
val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId)
|
||||
if (memberContact != null) {
|
||||
val memberChat = Chat(ChatInfo.Direct(memberContact), chatItems = arrayListOf())
|
||||
val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf())
|
||||
chatModel.addChat(memberChat)
|
||||
openLoadedChat(memberChat, chatModel)
|
||||
closeAll()
|
||||
@ -94,11 +95,11 @@ fun GroupMemberInfoView(
|
||||
}
|
||||
},
|
||||
connectViaAddress = { connReqUri ->
|
||||
connectViaMemberAddressAlert(connReqUri)
|
||||
connectViaMemberAddressAlert(rhId, connReqUri)
|
||||
},
|
||||
blockMember = { blockMemberAlert(groupInfo, member) },
|
||||
unblockMember = { unblockMemberAlert(groupInfo, member) },
|
||||
removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) },
|
||||
blockMember = { blockMemberAlert(rhId, groupInfo, member) },
|
||||
unblockMember = { unblockMemberAlert(rhId, groupInfo, member) },
|
||||
removeMember = { removeMemberDialog(rhId, groupInfo, member, chatModel, close) },
|
||||
onRoleSelected = {
|
||||
if (it == newRole.value) return@GroupMemberInfoLayout
|
||||
val prevValue = newRole.value
|
||||
@ -108,8 +109,8 @@ fun GroupMemberInfoView(
|
||||
}) {
|
||||
withApi {
|
||||
kotlin.runCatching {
|
||||
val mem = chatModel.controller.apiMemberRole(groupInfo.groupId, member.groupMemberId, it)
|
||||
chatModel.upsertGroupMember(groupInfo, mem)
|
||||
val mem = chatModel.controller.apiMemberRole(rhId, groupInfo.groupId, member.groupMemberId, it)
|
||||
chatModel.upsertGroupMember(rhId, groupInfo, mem)
|
||||
}.onFailure {
|
||||
newRole.value = prevValue
|
||||
}
|
||||
@ -119,10 +120,10 @@ fun GroupMemberInfoView(
|
||||
switchMemberAddress = {
|
||||
showSwitchAddressAlert(switchAddress = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@ -131,10 +132,10 @@ fun GroupMemberInfoView(
|
||||
abortSwitchMemberAddress = {
|
||||
showAbortSwitchAddressAlert(abortSwitchAddress = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@ -142,10 +143,10 @@ fun GroupMemberInfoView(
|
||||
},
|
||||
syncMemberConnection = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = false)
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@ -153,10 +154,10 @@ fun GroupMemberInfoView(
|
||||
syncMemberConnectionForce = {
|
||||
showSyncConnectionForceAlert(syncConnectionForce = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = true)
|
||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true)
|
||||
if (r != null) {
|
||||
connStats.value = r.second
|
||||
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
|
||||
chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
@ -170,9 +171,10 @@ fun GroupMemberInfoView(
|
||||
connectionCode,
|
||||
mem.verified,
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyGroupMember(mem.groupId, mem.groupMemberId, code)?.let { r ->
|
||||
chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r ->
|
||||
val (verified, existingCode) = r
|
||||
chatModel.upsertGroupMember(
|
||||
rhId,
|
||||
groupInfo,
|
||||
mem.copy(
|
||||
activeConn = mem.activeConn?.copy(
|
||||
@ -196,16 +198,16 @@ fun GroupMemberInfoView(
|
||||
}
|
||||
}
|
||||
|
||||
fun removeMemberDialog(groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.button_remove_member),
|
||||
text = generalGetString(MR.strings.member_will_be_removed_from_group_cannot_be_undone),
|
||||
confirmText = generalGetString(MR.strings.remove_member_confirmation),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val removedMember = chatModel.controller.apiRemoveMember(member.groupId, member.groupMemberId)
|
||||
val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId)
|
||||
if (removedMember != null) {
|
||||
chatModel.upsertGroupMember(groupInfo, removedMember)
|
||||
chatModel.upsertGroupMember(rhId, groupInfo, removedMember)
|
||||
}
|
||||
close?.invoke()
|
||||
}
|
||||
@ -500,11 +502,11 @@ private fun updateMemberRoleDialog(
|
||||
)
|
||||
}
|
||||
|
||||
fun connectViaMemberAddressAlert(connReqUri: String) {
|
||||
fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) {
|
||||
try {
|
||||
val uri = URI(connReqUri)
|
||||
withApi {
|
||||
planAndConnect(chatModel, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
|
||||
planAndConnect(chatModel, rhId, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@ -514,39 +516,39 @@ fun connectViaMemberAddressAlert(connReqUri: String) {
|
||||
}
|
||||
}
|
||||
|
||||
fun blockMemberAlert(gInfo: GroupInfo, mem: GroupMember) {
|
||||
fun blockMemberAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.block_member_question),
|
||||
text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName),
|
||||
confirmText = generalGetString(MR.strings.block_member_confirmation),
|
||||
onConfirm = {
|
||||
toggleShowMemberMessages(gInfo, mem, false)
|
||||
toggleShowMemberMessages(rhId, gInfo, mem, false)
|
||||
},
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
|
||||
fun unblockMemberAlert(gInfo: GroupInfo, mem: GroupMember) {
|
||||
fun unblockMemberAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.unblock_member_question),
|
||||
text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName),
|
||||
confirmText = generalGetString(MR.strings.unblock_member_confirmation),
|
||||
onConfirm = {
|
||||
toggleShowMemberMessages(gInfo, mem, true)
|
||||
toggleShowMemberMessages(rhId, gInfo, mem, true)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun toggleShowMemberMessages(gInfo: GroupInfo, member: GroupMember, showMessages: Boolean) {
|
||||
fun toggleShowMemberMessages(rhId: Long?, gInfo: GroupInfo, member: GroupMember, showMessages: Boolean) {
|
||||
val updatedMemberSettings = member.memberSettings.copy(showMessages = showMessages)
|
||||
updateMemberSettings(gInfo, member, updatedMemberSettings)
|
||||
updateMemberSettings(rhId, gInfo, member, updatedMemberSettings)
|
||||
}
|
||||
|
||||
fun updateMemberSettings(gInfo: GroupInfo, member: GroupMember, memberSettings: GroupMemberSettings) {
|
||||
fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, memberSettings: GroupMemberSettings) {
|
||||
withBGApi {
|
||||
val success = ChatController.apiSetMemberSettings(gInfo.groupId, member.groupMemberId, memberSettings)
|
||||
val success = ChatController.apiSetMemberSettings(rhId, gInfo.groupId, member.groupMemberId, memberSettings)
|
||||
if (success) {
|
||||
ChatModel.upsertGroupMember(gInfo, member.copy(memberSettings = memberSettings))
|
||||
ChatModel.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,12 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
|
||||
val groupInfo = remember { derivedStateOf { (m.getChat(chatId)?.chatInfo as? ChatInfo.Group)?.groupInfo } }
|
||||
fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> Unit,) {
|
||||
val groupInfo = remember { derivedStateOf {
|
||||
val ch = m.getChat(chatId)
|
||||
val g = (ch?.chatInfo as? ChatInfo.Group)?.groupInfo
|
||||
if (g == null || ch?.remoteHostId != rhId) null else g
|
||||
}}
|
||||
val gInfo = groupInfo.value ?: return
|
||||
var preferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(gInfo.fullGroupPreferences) }
|
||||
var currentPreferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(preferences) }
|
||||
@ -30,9 +34,9 @@ fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
|
||||
fun savePrefs(afterSave: () -> Unit = {}) {
|
||||
withApi {
|
||||
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
|
||||
val g = m.controller.apiUpdateGroup(gInfo.groupId, gp)
|
||||
val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp)
|
||||
if (g != null) {
|
||||
m.updateGroup(g)
|
||||
m.updateGroup(rhId, g)
|
||||
currentPreferences = preferences
|
||||
}
|
||||
afterSave()
|
||||
|
@ -30,15 +30,15 @@ import kotlinx.coroutines.launch
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun GroupProfileView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) {
|
||||
fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) {
|
||||
GroupProfileLayout(
|
||||
close = close,
|
||||
groupProfile = groupInfo.groupProfile,
|
||||
saveProfile = { p ->
|
||||
withApi {
|
||||
val gInfo = chatModel.controller.apiUpdateGroup(groupInfo.groupId, p)
|
||||
val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p)
|
||||
if (gInfo != null) {
|
||||
chatModel.updateGroup(gInfo)
|
||||
chatModel.updateGroup(rhId, gInfo)
|
||||
close.invoke()
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun GroupWelcomeView(m: ChatModel, groupInfo: GroupInfo, close: () -> Unit) {
|
||||
fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) {
|
||||
var gInfo by remember { mutableStateOf(groupInfo) }
|
||||
val welcomeText = remember { mutableStateOf(gInfo.groupProfile.description ?: "") }
|
||||
|
||||
@ -41,10 +41,10 @@ fun GroupWelcomeView(m: ChatModel, groupInfo: GroupInfo, close: () -> Unit) {
|
||||
welcome = null
|
||||
}
|
||||
val groupProfileUpdated = gInfo.groupProfile.copy(description = welcome)
|
||||
val res = m.controller.apiUpdateGroup(gInfo.groupId, groupProfileUpdated)
|
||||
val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated)
|
||||
if (res != null) {
|
||||
gInfo = res
|
||||
m.updateGroup(res)
|
||||
m.updateGroup(rhId, res)
|
||||
welcomeText.value = welcome ?: ""
|
||||
}
|
||||
afterSave()
|
||||
|
@ -94,13 +94,19 @@ fun CIFileView(
|
||||
)
|
||||
}
|
||||
is CIFileStatus.RcvComplete -> {
|
||||
val filePath = getLoadedFilePath(file)
|
||||
if (filePath != null) {
|
||||
withApi {
|
||||
saveFileLauncher.launch(file.fileName)
|
||||
withBGApi {
|
||||
var filePath = getLoadedFilePath(file)
|
||||
if (chatModel.connectedToRemote() && filePath == null) {
|
||||
file.loadRemoteFile(true)
|
||||
filePath = getLoadedFilePath(file)
|
||||
}
|
||||
if (filePath != null) {
|
||||
withApi {
|
||||
saveFileLauncher.launch(file.fileName)
|
||||
}
|
||||
} else {
|
||||
showToast(generalGetString(MR.strings.file_not_found))
|
||||
}
|
||||
} else {
|
||||
showToast(generalGetString(MR.strings.file_not_found))
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
|
@ -22,6 +22,9 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
|
||||
@ -134,7 +137,7 @@ fun CIImageView(
|
||||
return false
|
||||
}
|
||||
|
||||
fun imageAndFilePath(file: CIFile?): Triple<ImageBitmap, ByteArray, String>? {
|
||||
suspend fun imageAndFilePath(file: CIFile?): Triple<ImageBitmap, ByteArray, String>? {
|
||||
val res = getLoadedImage(file)
|
||||
if (res != null) {
|
||||
val (imageBitmap: ImageBitmap, data: ByteArray) = res
|
||||
@ -148,9 +151,23 @@ fun CIImageView(
|
||||
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
val res = remember(file) { imageAndFilePath(file) }
|
||||
if (res != null) {
|
||||
val (imageBitmap, data, _) = res
|
||||
val res: MutableState<Triple<ImageBitmap, ByteArray, String>?> = remember {
|
||||
mutableStateOf(
|
||||
if (chatModel.connectedToRemote()) null else runBlocking { imageAndFilePath(file) }
|
||||
)
|
||||
}
|
||||
if (chatModel.connectedToRemote()) {
|
||||
LaunchedEffect(file, CIFile.cachedRemoteFileRequests.toList()) {
|
||||
withBGApi {
|
||||
if (res.value == null || res.value!!.third != getLoadedFilePath(file)) {
|
||||
res.value = imageAndFilePath(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val loaded = res.value
|
||||
if (loaded != null) {
|
||||
val (imageBitmap, data, _) = loaded
|
||||
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, onClick) })
|
||||
} else {
|
||||
imageView(base64ToBitmap(image), onClick = {
|
||||
|
@ -21,6 +21,7 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
|
||||
@ -37,10 +38,21 @@ fun CIVideoView(
|
||||
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
val filePath = remember(file) { getLoadedFilePath(file) }
|
||||
val preview = remember(image) { base64ToBitmap(image) }
|
||||
if (file != null && filePath != null) {
|
||||
val uri = remember(filePath) { getAppFileUri(filePath.substringAfterLast(File.separator)) }
|
||||
val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) }
|
||||
if (chatModel.connectedToRemote()) {
|
||||
LaunchedEffect(file) {
|
||||
withBGApi {
|
||||
if (file != null && file.loaded && getLoadedFilePath(file) == null) {
|
||||
file.loadRemoteFile(false)
|
||||
filePath.value = getLoadedFilePath(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val f = filePath.value
|
||||
if (file != null && f != null) {
|
||||
val uri = remember(filePath) { getAppFileUri(f.substringAfterLast(File.separator)) }
|
||||
val view = LocalMultiplatformView()
|
||||
VideoView(uri, file, preview, duration * 1000L, showMenu, onClick = {
|
||||
hideKeyboard(view)
|
||||
|
@ -22,7 +22,7 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
// TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901
|
||||
|
||||
@ -44,16 +44,25 @@ fun CIVoiceView(
|
||||
) {
|
||||
if (file != null) {
|
||||
val f = file.fileSource?.filePath
|
||||
val fileSource = remember(f, file.fileStatus) { getLoadedFileSource(file) }
|
||||
val fileSource = remember(f, file.fileStatus, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFileSource(file)) }
|
||||
var brokenAudio by rememberSaveable(f) { mutableStateOf(false) }
|
||||
val audioPlaying = rememberSaveable(f) { mutableStateOf(false) }
|
||||
val progress = rememberSaveable(f) { mutableStateOf(0) }
|
||||
val duration = rememberSaveable(f) { mutableStateOf(providedDurationSec * 1000) }
|
||||
val play = {
|
||||
if (fileSource != null) {
|
||||
AudioPlayer.play(fileSource, audioPlaying, progress, duration, true)
|
||||
brokenAudio = !audioPlaying.value
|
||||
val play: () -> Unit = {
|
||||
val playIfExists = {
|
||||
if (fileSource.value != null) {
|
||||
AudioPlayer.play(fileSource.value!!, audioPlaying, progress, duration, true)
|
||||
brokenAudio = !audioPlaying.value
|
||||
}
|
||||
}
|
||||
if (chatModel.connectedToRemote() && fileSource.value == null) {
|
||||
withBGApi {
|
||||
file.loadRemoteFile(true)
|
||||
fileSource.value = getLoadedFileSource(file)
|
||||
playIfExists()
|
||||
}
|
||||
} else playIfExists()
|
||||
}
|
||||
val pause = {
|
||||
AudioPlayer.pause(audioPlaying, progress)
|
||||
@ -68,7 +77,7 @@ fun CIVoiceView(
|
||||
}
|
||||
}
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) {
|
||||
AudioPlayer.seekTo(it, progress, fileSource?.filePath)
|
||||
AudioPlayer.seekTo(it, progress, fileSource.value?.filePath)
|
||||
}
|
||||
} else {
|
||||
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick, receiveFile)
|
||||
|
@ -194,19 +194,34 @@ fun ChatItemView(
|
||||
})
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = {
|
||||
val fileSource = getLoadedFileSource(cItem.file)
|
||||
when {
|
||||
fileSource != null -> shareFile(cItem.text, fileSource)
|
||||
else -> clipboard.shareText(cItem.content.text)
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = {
|
||||
copyItemToClipboard(cItem, clipboard)
|
||||
showMenu.value = false
|
||||
})
|
||||
if ((cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) && getLoadedFilePath(cItem.file) != null) {
|
||||
val cachedRemoteReqs = remember { CIFile.cachedRemoteFileRequests }
|
||||
val copyAndShareAllowed = cItem.file == null || !chatModel.connectedToRemote() || getLoadedFilePath(cItem.file) != null || !cachedRemoteReqs.contains(cItem.file.fileSource)
|
||||
if (copyAndShareAllowed) {
|
||||
ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = {
|
||||
var fileSource = getLoadedFileSource(cItem.file)
|
||||
val shareIfExists = {
|
||||
when (val f = fileSource) {
|
||||
null -> clipboard.shareText(cItem.content.text)
|
||||
else -> shareFile(cItem.text, f)
|
||||
}
|
||||
showMenu.value = false
|
||||
}
|
||||
if (chatModel.connectedToRemote() && fileSource == null) {
|
||||
withBGApi {
|
||||
cItem.file?.loadRemoteFile(true)
|
||||
fileSource = getLoadedFileSource(cItem.file)
|
||||
shareIfExists()
|
||||
}
|
||||
} else shareIfExists()
|
||||
})
|
||||
}
|
||||
if (copyAndShareAllowed) {
|
||||
ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = {
|
||||
copyItemToClipboard(cItem, clipboard)
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
if ((cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) && (getLoadedFilePath(cItem.file) != null || (chatModel.connectedToRemote() && !cachedRemoteReqs.contains(cItem.file?.fileSource)))) {
|
||||
SaveContentItemAction(cItem, saveFileLauncher, showMenu)
|
||||
}
|
||||
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
|
||||
@ -590,7 +605,7 @@ private fun ShrinkItemAction(revealed: MutableState<Boolean>, showMenu: MutableS
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ItemAction(text: String, icon: Painter, onClick: () -> Unit, color: Color = Color.Unspecified) {
|
||||
fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, onClick: () -> Unit) {
|
||||
val finalColor = if (color == Color.Unspecified) {
|
||||
if (isInDarkTheme()) MenuTextColorDark else Color.Black
|
||||
} else color
|
||||
|
@ -62,7 +62,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode, inProgress = false, progressByTimeout = false) },
|
||||
click = { directChatAction(chat.chatInfo.contact, chatModel) },
|
||||
click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) },
|
||||
dropdownMenuItems = { ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead) },
|
||||
showMenu,
|
||||
stopped,
|
||||
@ -72,7 +72,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
is ChatInfo.Group ->
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode, inProgress.value, progressByTimeout) },
|
||||
click = { if (!inProgress.value) groupChatAction(chat.chatInfo.groupInfo, chatModel, inProgress) },
|
||||
click = { if (!inProgress.value) groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) },
|
||||
dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) },
|
||||
showMenu,
|
||||
stopped,
|
||||
@ -81,8 +81,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
is ChatInfo.ContactRequest ->
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ContactRequestView(chat.chatInfo) },
|
||||
click = { contactRequestAlertDialog(chat.chatInfo, chatModel) },
|
||||
dropdownMenuItems = { ContactRequestMenuItems(chat.chatInfo, chatModel, showMenu) },
|
||||
click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) },
|
||||
dropdownMenuItems = { ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) },
|
||||
showMenu,
|
||||
stopped,
|
||||
selectedChat
|
||||
@ -94,10 +94,10 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close ->
|
||||
ContactConnectionInfoView(chatModel, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close)
|
||||
ContactConnectionInfoView(chatModel, chat.remoteHostId, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close)
|
||||
}
|
||||
},
|
||||
dropdownMenuItems = { ContactConnectionMenuItems(chat.chatInfo, chatModel, showMenu) },
|
||||
dropdownMenuItems = { ContactConnectionMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) },
|
||||
showMenu,
|
||||
stopped,
|
||||
selectedChat
|
||||
@ -119,38 +119,38 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
fun directChatAction(contact: Contact, chatModel: ChatModel) {
|
||||
fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
|
||||
when {
|
||||
contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, contact, close = null, openChat = true)
|
||||
else -> withBGApi { openChat(ChatInfo.Direct(contact), chatModel) }
|
||||
contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
|
||||
else -> withBGApi { openChat(rhId, ChatInfo.Direct(contact), chatModel) }
|
||||
}
|
||||
}
|
||||
|
||||
fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
|
||||
fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
|
||||
when (groupInfo.membership.memberStatus) {
|
||||
GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(groupInfo, chatModel, inProgress)
|
||||
GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress)
|
||||
GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert()
|
||||
else -> withBGApi { openChat(ChatInfo.Group(groupInfo), chatModel) }
|
||||
else -> withBGApi { openChat(rhId, ChatInfo.Group(groupInfo), chatModel) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) {
|
||||
val chat = chatModel.controller.apiGetChat(ChatType.Direct, contactId)
|
||||
suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) {
|
||||
val chat = chatModel.controller.apiGetChat(rhId, ChatType.Direct, contactId)
|
||||
if (chat != null) {
|
||||
openLoadedChat(chat, chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openGroupChat(groupId: Long, chatModel: ChatModel) {
|
||||
val chat = chatModel.controller.apiGetChat(ChatType.Group, groupId)
|
||||
suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) {
|
||||
val chat = chatModel.controller.apiGetChat(rhId, ChatType.Group, groupId)
|
||||
if (chat != null) {
|
||||
openLoadedChat(chat, chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
|
||||
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId)
|
||||
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId)
|
||||
if (chat != null) {
|
||||
openLoadedChat(chat, chatModel)
|
||||
Log.d(TAG, "TODOCHAT: openChat: opened ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
|
||||
@ -164,22 +164,24 @@ fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
|
||||
chatModel.chatId.value = chat.chatInfo.id
|
||||
}
|
||||
|
||||
suspend fun apiLoadPrevMessages(chatInfo: ChatInfo, chatModel: ChatModel, beforeChatItemId: Long, search: String) {
|
||||
suspend fun apiLoadPrevMessages(ch: Chat, chatModel: ChatModel, beforeChatItemId: Long, search: String) {
|
||||
val chatInfo = ch.chatInfo
|
||||
val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT)
|
||||
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return
|
||||
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return
|
||||
if (chatModel.chatId.value != chat.id) return
|
||||
chatModel.chatItems.addAll(0, chat.chatItems)
|
||||
}
|
||||
|
||||
suspend fun apiFindMessages(chatInfo: ChatInfo, chatModel: ChatModel, search: String) {
|
||||
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, search = search) ?: return
|
||||
suspend fun apiFindMessages(ch: Chat, chatModel: ChatModel, search: String) {
|
||||
val chatInfo = ch.chatInfo
|
||||
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return
|
||||
if (chatModel.chatId.value != chat.id) return
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItems.addAll(0, chat.chatItems)
|
||||
}
|
||||
|
||||
suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
val groupMembers = chatModel.controller.apiListMembers(groupInfo.groupId)
|
||||
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
val groupMembers = chatModel.controller.apiListMembers(rhId, groupInfo.groupId)
|
||||
val currentMembers = chatModel.groupMembers
|
||||
val newMembers = groupMembers.map { newMember ->
|
||||
val currentMember = currentMembers.find { it.id == newMember.id }
|
||||
@ -230,7 +232,7 @@ fun GroupMenuItems(
|
||||
}
|
||||
GroupMemberStatus.MemAccepted -> {
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
LeaveGroupAction(groupInfo, chatModel, showMenu)
|
||||
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
|
||||
}
|
||||
if (groupInfo.canDelete) {
|
||||
DeleteGroupAction(chat, groupInfo, chatModel, showMenu)
|
||||
@ -246,7 +248,7 @@ fun GroupMenuItems(
|
||||
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
||||
ClearChatAction(chat, chatModel, showMenu)
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
LeaveGroupAction(groupInfo, chatModel, showMenu)
|
||||
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
|
||||
}
|
||||
if (groupInfo.canDelete) {
|
||||
DeleteGroupAction(chat, groupInfo, chatModel, showMenu)
|
||||
@ -310,7 +312,7 @@ fun ClearChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boo
|
||||
stringResource(MR.strings.clear_chat_menu_action),
|
||||
painterResource(MR.images.ic_settings_backup_restore),
|
||||
onClick = {
|
||||
clearChatDialog(chat.chatInfo, chatModel)
|
||||
clearChatDialog(chat, chatModel)
|
||||
showMenu.value = false
|
||||
},
|
||||
color = WarningOrange
|
||||
@ -323,7 +325,7 @@ fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState
|
||||
stringResource(MR.strings.delete_contact_menu_action),
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
deleteContactDialog(chat.chatInfo, chatModel)
|
||||
deleteContactDialog(chat, chatModel)
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
@ -336,7 +338,7 @@ fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, sh
|
||||
stringResource(MR.strings.delete_group_menu_action),
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
deleteGroupDialog(chat.chatInfo, groupInfo, chatModel)
|
||||
deleteGroupDialog(chat, groupInfo, chatModel)
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
@ -354,7 +356,7 @@ fun JoinGroupAction(
|
||||
val joinGroup: () -> Unit = {
|
||||
withApi {
|
||||
inProgress.value = true
|
||||
chatModel.controller.apiJoinGroup(groupInfo.groupId)
|
||||
chatModel.controller.apiJoinGroup(chat.remoteHostId, groupInfo.groupId)
|
||||
inProgress.value = false
|
||||
}
|
||||
}
|
||||
@ -370,12 +372,12 @@ fun JoinGroupAction(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
fun LeaveGroupAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.leave_group_button),
|
||||
painterResource(MR.images.ic_logout),
|
||||
onClick = {
|
||||
leaveGroupDialog(groupInfo, chatModel)
|
||||
leaveGroupDialog(rhId, groupInfo, chatModel)
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
@ -383,13 +385,13 @@ fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: Mutab
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.accept_contact_button),
|
||||
painterResource(MR.images.ic_check),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
acceptContactRequest(incognito = false, chatInfo.apiId, chatInfo, true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
@ -398,7 +400,7 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
|
||||
painterResource(MR.images.ic_theater_comedy),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
acceptContactRequest(incognito = true, chatInfo.apiId, chatInfo, true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
@ -406,7 +408,7 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
|
||||
stringResource(MR.strings.reject_contact_button),
|
||||
painterResource(MR.images.ic_close),
|
||||
onClick = {
|
||||
rejectContactRequest(chatInfo, chatModel)
|
||||
rejectContactRequest(rhId, chatInfo, chatModel)
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
@ -414,7 +416,7 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
fun ContactConnectionMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactConnection, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.set_contact_name),
|
||||
painterResource(MR.images.ic_edit),
|
||||
@ -422,7 +424,7 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close ->
|
||||
ContactConnectionInfoView(chatModel, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close)
|
||||
ContactConnectionInfoView(chatModel, rhId, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close)
|
||||
}
|
||||
showMenu.value = false
|
||||
},
|
||||
@ -431,7 +433,7 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
|
||||
stringResource(MR.strings.delete_verb),
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {
|
||||
deleteContactConnectionAlert(rhId, chatInfo.contactConnection, chatModel) {
|
||||
if (chatModel.chatId.value == null) {
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
@ -471,8 +473,9 @@ fun markChatRead(c: Chat, chatModel: ChatModel) {
|
||||
withApi {
|
||||
if (chat.chatStats.unreadCount > 0) {
|
||||
val minUnreadItemId = chat.chatStats.minUnreadItemId
|
||||
chatModel.markChatItemsRead(chat.chatInfo)
|
||||
chatModel.markChatItemsRead(chat)
|
||||
chatModel.controller.apiChatRead(
|
||||
chat.remoteHostId,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
CC.ItemRange(minUnreadItemId, chat.chatItems.last().id)
|
||||
@ -481,12 +484,13 @@ fun markChatRead(c: Chat, chatModel: ChatModel) {
|
||||
}
|
||||
if (chat.chatStats.unreadChat) {
|
||||
val success = chatModel.controller.apiChatUnread(
|
||||
chat.remoteHostId,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
false
|
||||
)
|
||||
if (success) {
|
||||
chatModel.replaceChat(chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||
chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -498,17 +502,18 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
|
||||
|
||||
withApi {
|
||||
val success = chatModel.controller.apiChatUnread(
|
||||
chat.remoteHostId,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
true
|
||||
)
|
||||
if (success) {
|
||||
chatModel.replaceChat(chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
|
||||
chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
|
||||
fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.accept_connection_request__question),
|
||||
text = AnnotatedString(generalGetString(MR.strings.if_you_choose_to_reject_the_sender_will_not_be_notified)),
|
||||
@ -516,19 +521,19 @@ fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
acceptContactRequest(incognito = false, contactRequest.apiId, contactRequest, true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
acceptContactRequest(incognito = true, contactRequest.apiId, contactRequest, true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
rejectContactRequest(contactRequest, chatModel)
|
||||
rejectContactRequest(rhId, contactRequest, chatModel)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.reject_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
|
||||
}
|
||||
@ -537,24 +542,24 @@ fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel
|
||||
)
|
||||
}
|
||||
|
||||
fun acceptContactRequest(incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) {
|
||||
fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) {
|
||||
withApi {
|
||||
val contact = chatModel.controller.apiAcceptContactRequest(incognito, apiId)
|
||||
val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId)
|
||||
if (contact != null && isCurrentUser && contactRequest != null) {
|
||||
val chat = Chat(ChatInfo.Direct(contact), listOf())
|
||||
chatModel.replaceChat(contactRequest.id, chat)
|
||||
val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf())
|
||||
chatModel.replaceChat(rhId, contactRequest.id, chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun rejectContactRequest(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
|
||||
fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
|
||||
withApi {
|
||||
chatModel.controller.apiRejectContactRequest(contactRequest.apiId)
|
||||
chatModel.removeChat(contactRequest.id)
|
||||
chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId)
|
||||
chatModel.removeChat(rhId, contactRequest.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) {
|
||||
fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.delete_pending_connection__question),
|
||||
text = generalGetString(
|
||||
@ -565,8 +570,8 @@ fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel
|
||||
onConfirm = {
|
||||
withApi {
|
||||
AlertManager.shared.hideAlert()
|
||||
if (chatModel.controller.apiDeleteChat(ChatType.ContactConnection, connection.apiId)) {
|
||||
chatModel.removeChat(connection.id)
|
||||
if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) {
|
||||
chatModel.removeChat(rhId, connection.id)
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
@ -575,16 +580,17 @@ fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel
|
||||
)
|
||||
}
|
||||
|
||||
fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
// TODO why is it not used
|
||||
fun pendingContactAlertDialog(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.alert_title_contact_connection_pending),
|
||||
text = generalGetString(MR.strings.alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry),
|
||||
confirmText = generalGetString(MR.strings.button_delete_contact),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId)
|
||||
val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(chatInfo.id)
|
||||
chatModel.removeChat(rhId, chatInfo.id)
|
||||
if (chatModel.chatId.value == chatInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
@ -599,6 +605,7 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
|
||||
fun askCurrentOrIncognitoProfileConnectContactViaAddress(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
contact: Contact,
|
||||
close: (() -> Unit)?,
|
||||
openChat: Boolean
|
||||
@ -611,9 +618,9 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
|
||||
AlertManager.shared.hideAlert()
|
||||
withApi {
|
||||
close?.invoke()
|
||||
val ok = connectContactViaAddress(chatModel, contact.contactId, incognito = false)
|
||||
val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false)
|
||||
if (ok && openChat) {
|
||||
openDirectChat(contact.contactId, chatModel)
|
||||
openDirectChat(rhId, contact.contactId, chatModel)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
@ -623,9 +630,9 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
|
||||
AlertManager.shared.hideAlert()
|
||||
withApi {
|
||||
close?.invoke()
|
||||
val ok = connectContactViaAddress(chatModel, contact.contactId, incognito = true)
|
||||
val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true)
|
||||
if (ok && openChat) {
|
||||
openDirectChat(contact.contactId, chatModel)
|
||||
openDirectChat(rhId, contact.contactId, chatModel)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
@ -641,10 +648,10 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun connectContactViaAddress(chatModel: ChatModel, contactId: Long, incognito: Boolean): Boolean {
|
||||
val contact = chatModel.controller.apiConnectContactViaAddress(incognito, contactId)
|
||||
suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactId: Long, incognito: Boolean): Boolean {
|
||||
val contact = chatModel.controller.apiConnectContactViaAddress(rhId, incognito, contactId)
|
||||
if (contact != null) {
|
||||
chatModel.updateContact(contact)
|
||||
chatModel.updateContact(rhId, contact)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.connection_request_sent),
|
||||
text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted)
|
||||
@ -654,7 +661,7 @@ suspend fun connectContactViaAddress(chatModel: ChatModel, contactId: Long, inco
|
||||
return false
|
||||
}
|
||||
|
||||
fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
|
||||
fun acceptGroupInvitationAlertDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.join_group_question),
|
||||
text = generalGetString(MR.strings.you_are_invited_to_group_join_to_connect_with_group_members),
|
||||
@ -662,12 +669,12 @@ fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel,
|
||||
onConfirm = {
|
||||
withApi {
|
||||
inProgress?.value = true
|
||||
chatModel.controller.apiJoinGroup(groupInfo.groupId)
|
||||
chatModel.controller.apiJoinGroup(rhId, groupInfo.groupId)
|
||||
inProgress?.value = false
|
||||
}
|
||||
},
|
||||
dismissText = generalGetString(MR.strings.delete_verb),
|
||||
onDismiss = { deleteGroup(groupInfo, chatModel) }
|
||||
onDismiss = { deleteGroup(rhId, groupInfo, chatModel) }
|
||||
)
|
||||
}
|
||||
|
||||
@ -679,11 +686,11 @@ fun cantInviteIncognitoAlert() {
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteGroup(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiDeleteChat(ChatType.Group, groupInfo.apiId)
|
||||
val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId)
|
||||
if (r) {
|
||||
chatModel.removeChat(groupInfo.id)
|
||||
chatModel.removeChat(rhId, groupInfo.id)
|
||||
if (chatModel.chatId.value == groupInfo.id) {
|
||||
chatModel.chatId.value = null
|
||||
ModalManager.end.closeModals()
|
||||
@ -723,15 +730,15 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo
|
||||
withApi {
|
||||
val res = when (newChatInfo) {
|
||||
is ChatInfo.Direct -> with(newChatInfo) {
|
||||
chatModel.controller.apiSetSettings(chatType, apiId, contact.chatSettings)
|
||||
chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, contact.chatSettings)
|
||||
}
|
||||
is ChatInfo.Group -> with(newChatInfo) {
|
||||
chatModel.controller.apiSetSettings(chatType, apiId, groupInfo.chatSettings)
|
||||
chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, groupInfo.chatSettings)
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
if (res && newChatInfo != null) {
|
||||
chatModel.updateChatInfo(newChatInfo)
|
||||
chatModel.updateChatInfo(chat.remoteHostId, newChatInfo)
|
||||
if (chatSettings.enableNtfs != MsgFilter.All) {
|
||||
ntfManager.cancelNotificationsForChat(chat.id)
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
@ -9,30 +8,22 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.SettingsViewState
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.WhatsNewView
|
||||
import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
||||
import chat.simplex.common.views.usersettings.SettingsView
|
||||
import chat.simplex.common.views.usersettings.simplexTeamUri
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.Call
|
||||
import chat.simplex.common.views.call.CallMediaType
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
@ -62,7 +53,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
val url = chatModel.appOpenUrl.value
|
||||
if (url != null) {
|
||||
chatModel.appOpenUrl.value = null
|
||||
connectIfOpenedViaUri(url, chatModel)
|
||||
connectIfOpenedViaUri(chatModel.remoteHostId, url, chatModel)
|
||||
}
|
||||
}
|
||||
if (appPlatform.isDesktop) {
|
||||
@ -77,7 +68,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val scope = rememberCoroutineScope()
|
||||
val (userPickerState, scaffoldState, switchingUsers ) = settingsState
|
||||
val (userPickerState, scaffoldState, switchingUsersAndHosts ) = settingsState
|
||||
Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) },
|
||||
@ -113,7 +104,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
) {
|
||||
if (chatModel.chats.isNotEmpty()) {
|
||||
ChatList(chatModel, search = searchInList)
|
||||
} else if (!switchingUsers.value) {
|
||||
} else if (!switchingUsersAndHosts.value) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) {
|
||||
OnboardingButtons(showNewChatSheet)
|
||||
@ -126,14 +117,16 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
}
|
||||
if (searchInList.isEmpty()) {
|
||||
DesktopActiveCallOverlayLayout(newChatSheetState)
|
||||
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
||||
// TODO disable this button and sheet for the duration of the switch
|
||||
NewChatSheet(chatModel, chatModel.remoteHostId, newChatSheetState, stopped, hideNewChatSheet)
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
UserPicker(chatModel, userPickerState, switchingUsers) {
|
||||
UserPicker(chatModel, userPickerState, switchingUsersAndHosts) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
}
|
||||
if (switchingUsers.value) {
|
||||
if (switchingUsersAndHosts.value) {
|
||||
Box(
|
||||
Modifier.fillMaxSize().clickable(enabled = false, onClick = {}),
|
||||
contentAlignment = Alignment.Center
|
||||
@ -224,7 +217,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
|
||||
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
||||
.all { u -> u.unreadCount == 0 }
|
||||
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
||||
if (users.size == 1) {
|
||||
if (users.size == 1 && chatModel.remoteHosts.isEmpty()) {
|
||||
scope.launch { drawerState.open() }
|
||||
} else {
|
||||
userPickerState.value = AnimatedViewState.VISIBLE
|
||||
@ -254,14 +247,25 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
|
||||
|
||||
@Composable
|
||||
fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
|
||||
IconButton(onClick = onButtonClicked) {
|
||||
Box {
|
||||
ProfileImage(
|
||||
image = image,
|
||||
size = 37.dp
|
||||
)
|
||||
if (!allRead) {
|
||||
unreadBadge()
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = onButtonClicked) {
|
||||
Box {
|
||||
ProfileImage(
|
||||
image = image,
|
||||
size = 37.dp
|
||||
)
|
||||
if (!allRead) {
|
||||
unreadBadge()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (appPlatform.isDesktop) {
|
||||
val h by remember { chatModel.currentRemoteHost }
|
||||
if (h != null) {
|
||||
Spacer(Modifier.width(12.dp))
|
||||
HostDisconnectButton {
|
||||
stopRemoteHostAndReloadHosts(h!!, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -314,13 +318,13 @@ private fun ProgressIndicator() {
|
||||
@Composable
|
||||
expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>)
|
||||
|
||||
fun connectIfOpenedViaUri(uri: URI, chatModel: ChatModel) {
|
||||
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
||||
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||
if (chatModel.currentUser.value == null) {
|
||||
chatModel.appOpenUrl.value = uri
|
||||
} else {
|
||||
withApi {
|
||||
planAndConnect(chatModel, uri, incognito = null, close = null)
|
||||
planAndConnect(chatModel, rhId, uri, incognito = null, close = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,13 +20,13 @@ fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
is ChatInfo.Direct ->
|
||||
ShareListNavLinkLayout(
|
||||
chatLinkPreview = { SharePreviewView(chat) },
|
||||
click = { directChatAction(chat.chatInfo.contact, chatModel) },
|
||||
click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) },
|
||||
stopped
|
||||
)
|
||||
is ChatInfo.Group ->
|
||||
ShareListNavLinkLayout(
|
||||
chatLinkPreview = { SharePreviewView(chat) },
|
||||
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
|
||||
click = { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) },
|
||||
stopped
|
||||
)
|
||||
is ChatInfo.ContactRequest, is ChatInfo.ContactConnection, is ChatInfo.InvalidJSON -> {}
|
||||
|
@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@Composable
|
||||
fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) {
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val (userPickerState, scaffoldState, switchingUsers) = settingsState
|
||||
val (userPickerState, scaffoldState, switchingUsersAndHosts) = settingsState
|
||||
val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp
|
||||
Scaffold(
|
||||
Modifier.padding(end = endPadding),
|
||||
@ -47,8 +47,9 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
UserPicker(chatModel, userPickerState, switchingUsers, showSettings = false, showCancel = true, cancelClicked = {
|
||||
UserPicker(chatModel, userPickerState, switchingUsersAndHosts, showSettings = false, showCancel = true, cancelClicked = {
|
||||
chatModel.sharedContent.value = null
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -72,7 +73,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
|
||||
val navButton: @Composable RowScope.() -> Unit = {
|
||||
when {
|
||||
showSearch -> NavigationButtonBack(hideSearchOnBack)
|
||||
users.size > 1 -> {
|
||||
users.size > 1 || chatModel.remoteHosts.isNotEmpty() -> {
|
||||
val allRead = users
|
||||
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
||||
.all { u -> u.unreadCount == 0 }
|
||||
|
@ -4,10 +4,12 @@ import SectionItemView
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@ -18,12 +20,16 @@ import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.User
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.remote.ConnectDesktopView
|
||||
import chat.simplex.common.views.remote.connectMobileDevice
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
@ -33,10 +39,11 @@ import kotlin.math.roundToInt
|
||||
fun UserPicker(
|
||||
chatModel: ChatModel,
|
||||
userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||
switchingUsers: MutableState<Boolean>,
|
||||
switchingUsersAndHosts: MutableState<Boolean>,
|
||||
showSettings: Boolean = true,
|
||||
showCancel: Boolean = false,
|
||||
cancelClicked: () -> Unit = {},
|
||||
useFromDesktopClicked: () -> Unit = {},
|
||||
settingsClicked: () -> Unit = {},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
@ -53,6 +60,12 @@ fun UserPicker(
|
||||
.sortedByDescending { it.user.activeUser }
|
||||
}
|
||||
}
|
||||
val remoteHosts by remember {
|
||||
derivedStateOf {
|
||||
chatModel.remoteHosts
|
||||
.sortedBy { it.hostDeviceName }
|
||||
}
|
||||
}
|
||||
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
@ -71,7 +84,7 @@ fun UserPicker(
|
||||
.filter { it }
|
||||
.collect {
|
||||
try {
|
||||
val updatedUsers = chatModel.controller.listUsers().sortedByDescending { it.user.activeUser }
|
||||
val updatedUsers = chatModel.controller.listUsers(chatModel.remoteHostId).sortedByDescending { it.user.activeUser }
|
||||
var same = users.size == updatedUsers.size
|
||||
if (same) {
|
||||
for (i in 0 until minOf(users.size, updatedUsers.size)) {
|
||||
@ -90,8 +103,42 @@ fun UserPicker(
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error updating users ${e.stackTraceToString()}")
|
||||
}
|
||||
if (!appPlatform.isDesktop) return@collect
|
||||
try {
|
||||
val updatedHosts = chatModel.controller.listRemoteHosts()?.sortedBy { it.hostDeviceName } ?: emptyList()
|
||||
if (remoteHosts != updatedHosts) {
|
||||
chatModel.remoteHosts.clear()
|
||||
chatModel.remoteHosts.addAll(updatedHosts)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error updating remote hosts ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
controller.reloadRemoteHosts()
|
||||
}
|
||||
val UsersView: @Composable ColumnScope.() -> Unit = {
|
||||
users.forEach { u ->
|
||||
UserProfilePickerItem(u.user, u.unreadCount, openSettings = settingsClicked) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
if (!u.user.activeUser) {
|
||||
scope.launch {
|
||||
val job = launch {
|
||||
delay(500)
|
||||
switchingUsersAndHosts.value = true
|
||||
}
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
chatModel.controller.changeActiveUser(u.user.remoteHostId, u.user.userId, null)
|
||||
job.cancel()
|
||||
switchingUsersAndHosts.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp))
|
||||
}
|
||||
}
|
||||
val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() }
|
||||
val maxWidth = with(LocalDensity.current) { windowWidth() * density }
|
||||
Box(Modifier
|
||||
@ -113,48 +160,72 @@ fun UserPicker(
|
||||
.background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp)))
|
||||
.clip(RoundedCornerShape(corner = CornerSize(25.dp)))
|
||||
) {
|
||||
val currentRemoteHost = remember { chatModel.currentRemoteHost }.value
|
||||
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
|
||||
users.forEach { u ->
|
||||
UserProfilePickerItem(u.user, u.unreadCount, PaddingValues(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING), openSettings = {
|
||||
settingsClicked()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
if (!u.user.activeUser) {
|
||||
scope.launch {
|
||||
val job = launch {
|
||||
delay(500)
|
||||
switchingUsers.value = true
|
||||
}
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
chatModel.controller.changeActiveUser(u.user.userId, null)
|
||||
job.cancel()
|
||||
switchingUsers.value = false
|
||||
}
|
||||
if (remoteHosts.isNotEmpty()) {
|
||||
if (currentRemoteHost == null) {
|
||||
LocalDevicePickerItem(true) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
switchToLocalDevice()
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
} else {
|
||||
val connecting = rememberSaveable { mutableStateOf(false) }
|
||||
RemoteHostPickerItem(currentRemoteHost,
|
||||
actionButtonClick = {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
stopRemoteHostAndReloadHosts(currentRemoteHost, true)
|
||||
}) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
switchToRemoteHost(currentRemoteHost, switchingUsersAndHosts, connecting)
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
}
|
||||
}
|
||||
|
||||
UsersView()
|
||||
|
||||
if (remoteHosts.isNotEmpty() && currentRemoteHost != null) {
|
||||
LocalDevicePickerItem(false) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
switchToLocalDevice()
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp))
|
||||
}
|
||||
remoteHosts.filter { !it.activeHost }.forEach { h ->
|
||||
val connecting = rememberSaveable { mutableStateOf(false) }
|
||||
RemoteHostPickerItem(h,
|
||||
actionButtonClick = {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
stopRemoteHostAndReloadHosts(h, false)
|
||||
}) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
switchToRemoteHost(h, switchingUsersAndHosts, connecting)
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
}
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
UseFromDesktopPickerItem {
|
||||
ModalManager.start.showCustomModal { close ->
|
||||
ConnectDesktopView(close)
|
||||
}
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
}
|
||||
if (showSettings) {
|
||||
SettingsPickerItem {
|
||||
settingsClicked()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
SettingsPickerItem(settingsClicked)
|
||||
}
|
||||
if (showCancel) {
|
||||
CancelPickerItem {
|
||||
cancelClicked()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
CancelPickerItem(cancelClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues = PaddingValues(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING), onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
|
||||
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@ -166,7 +237,7 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues
|
||||
indication = if (!u.activeUser) LocalIndication.current else null
|
||||
)
|
||||
.onRightClick { onLongClick() }
|
||||
.padding(padding),
|
||||
.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@ -219,16 +290,107 @@ fun UserProfileRow(u: User) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RemoteHostPickerItem(h: RemoteHostInfo, onLongClick: () -> Unit = {}, actionButtonClick: () -> Unit = {}, onClick: () -> Unit) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = if (h.activeHost) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified)
|
||||
.sizeIn(minHeight = 46.dp)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
.onRightClick { onLongClick() }
|
||||
.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RemoteHostRow(h)
|
||||
if (h.sessionState is RemoteHostSessionState.Connected) {
|
||||
HostDisconnectButton(actionButtonClick)
|
||||
} else {
|
||||
Box(Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RemoteHostRow(h: RemoteHostInfo) {
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = windowWidth() * 0.7f)
|
||||
.padding(start = 17.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_smartphone_300), h.hostDeviceName, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
Text(
|
||||
h.hostDeviceName,
|
||||
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
|
||||
color = if (h.activeHost) MaterialTheme.colors.onBackground else if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LocalDevicePickerItem(active: Boolean, onLongClick: () -> Unit = {}, onClick: () -> Unit) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = if (active) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified)
|
||||
.sizeIn(minHeight = 46.dp)
|
||||
.combinedClickable(
|
||||
onClick = if (active) {{}} else onClick,
|
||||
onLongClick = onLongClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = if (!active) LocalIndication.current else null
|
||||
)
|
||||
.onRightClick { onLongClick() }
|
||||
.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
LocalDeviceRow(active)
|
||||
Box(Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LocalDeviceRow(active: Boolean) {
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = windowWidth() * 0.7f)
|
||||
.padding(start = 17.dp, end = DEFAULT_PADDING),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(painterResource(MR.images.ic_desktop), stringResource(MR.strings.this_device), Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
Text(
|
||||
stringResource(MR.strings.this_device),
|
||||
modifier = Modifier.padding(start = 26.dp, end = 8.dp),
|
||||
color = if (active) MaterialTheme.colors.onBackground else if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UseFromDesktopPickerItem(onClick: () -> Unit) {
|
||||
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
||||
val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current)
|
||||
Icon(painterResource(MR.images.ic_desktop), 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
|
||||
private fun SettingsPickerItem(onClick: () -> Unit) {
|
||||
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) {
|
||||
val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current)
|
||||
Icon(painterResource(MR.images.ic_settings), 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,
|
||||
)
|
||||
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,9 +400,47 @@ private fun CancelPickerItem(onClick: () -> Unit) {
|
||||
val text = generalGetString(MR.strings.cancel_verb)
|
||||
Icon(painterResource(MR.images.ic_close), 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,
|
||||
Text(text, color = if (isInDarkTheme()) MenuTextColorDark else Color.Black)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HostDisconnectButton(onClick: (() -> Unit)?) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val hovered = interactionSource.collectIsHoveredAsState().value
|
||||
IconButton(onClick ?: {}, Modifier.requiredSize(20.dp), enabled = onClick != null) {
|
||||
Icon(
|
||||
painterResource(if (onClick == null) MR.images.ic_desktop else if (hovered) MR.images.ic_wifi_off else MR.images.ic_wifi),
|
||||
null,
|
||||
Modifier.size(20.dp).hoverable(interactionSource),
|
||||
tint = if (hovered && onClick != null) WarningOrange else MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchToLocalDevice() {
|
||||
withBGApi {
|
||||
chatController.switchUIRemoteHost(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchToRemoteHost(h: RemoteHostInfo, switchingUsersAndHosts: MutableState<Boolean>, connecting: MutableState<Boolean>) {
|
||||
if (!h.activeHost()) {
|
||||
withBGApi {
|
||||
val job = launch {
|
||||
delay(500)
|
||||
switchingUsersAndHosts.value = true
|
||||
}
|
||||
ModalManager.closeAllModalsEverywhere()
|
||||
if (h.sessionState != null) {
|
||||
chatModel.controller.switchUIRemoteHost(h.remoteHostId)
|
||||
} else {
|
||||
connectMobileDevice(h, connecting)
|
||||
}
|
||||
job.cancel()
|
||||
switchingUsersAndHosts.value = false
|
||||
}
|
||||
} else {
|
||||
connectMobileDevice(h, connecting)
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,8 @@ fun DatabaseView(
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
) {
|
||||
val user = m.currentUser.value
|
||||
val rhId = user?.remoteHostId
|
||||
DatabaseLayout(
|
||||
progressIndicator.value,
|
||||
remember { m.chatRunning }.value != false,
|
||||
@ -80,7 +82,7 @@ fun DatabaseView(
|
||||
chatLastStart,
|
||||
appFilesCountAndSize,
|
||||
chatItemTTL,
|
||||
m.currentUser.value,
|
||||
user,
|
||||
m.users,
|
||||
startChat = { startChat(m, chatLastStart, m.chatDbChanged) },
|
||||
stopChatAlert = { stopChatAlert(m) },
|
||||
@ -91,9 +93,9 @@ fun DatabaseView(
|
||||
val oldValue = chatItemTTL.value
|
||||
chatItemTTL.value = it
|
||||
if (it < oldValue) {
|
||||
setChatItemTTLAlert(m, chatItemTTL, progressIndicator, appFilesCountAndSize)
|
||||
setChatItemTTLAlert(m, rhId, chatItemTTL, progressIndicator, appFilesCountAndSize)
|
||||
} else if (it != oldValue) {
|
||||
setCiTTL(m, chatItemTTL, progressIndicator, appFilesCountAndSize)
|
||||
setCiTTL(m, rhId, chatItemTTL, progressIndicator, appFilesCountAndSize)
|
||||
}
|
||||
},
|
||||
showSettingsModal
|
||||
@ -265,7 +267,7 @@ fun DatabaseLayout(
|
||||
}
|
||||
|
||||
private fun setChatItemTTLAlert(
|
||||
m: ChatModel, selectedChatItemTTL: MutableState<ChatItemTTL>,
|
||||
m: ChatModel, rhId: Long?, selectedChatItemTTL: MutableState<ChatItemTTL>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
||||
) {
|
||||
@ -273,7 +275,7 @@ private fun setChatItemTTLAlert(
|
||||
title = generalGetString(MR.strings.enable_automatic_deletion_question),
|
||||
text = generalGetString(MR.strings.enable_automatic_deletion_message),
|
||||
confirmText = generalGetString(MR.strings.delete_messages),
|
||||
onConfirm = { setCiTTL(m, selectedChatItemTTL, progressIndicator, appFilesCountAndSize) },
|
||||
onConfirm = { setCiTTL(m, rhId, selectedChatItemTTL, progressIndicator, appFilesCountAndSize) },
|
||||
onDismiss = { selectedChatItemTTL.value = m.chatItemTTL.value },
|
||||
destructive = true,
|
||||
)
|
||||
@ -592,6 +594,7 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) {
|
||||
|
||||
private fun setCiTTL(
|
||||
m: ChatModel,
|
||||
rhId: Long?,
|
||||
chatItemTTL: MutableState<ChatItemTTL>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
||||
@ -600,7 +603,7 @@ private fun setCiTTL(
|
||||
progressIndicator.value = true
|
||||
withApi {
|
||||
try {
|
||||
m.controller.setChatItemTTL(chatItemTTL.value)
|
||||
m.controller.setChatItemTTL(rhId, chatItemTTL.value)
|
||||
// Update model on success
|
||||
m.chatItemTTL.value = chatItemTTL.value
|
||||
afterSetCiTTL(m, progressIndicator, appFilesCountAndSize)
|
||||
@ -623,7 +626,8 @@ private fun afterSetCiTTL(
|
||||
withApi {
|
||||
try {
|
||||
updatingChatsMutex.withLock {
|
||||
val chats = m.controller.apiGetChats()
|
||||
// this is using current remote host on purpose - if it changes during update, it will load correct chats
|
||||
val chats = m.controller.apiGetChats(m.remoteHostId)
|
||||
m.updateChats(chats)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -188,6 +188,25 @@ class AlertManager {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showAlertMsgWithProgress(
|
||||
title: String,
|
||||
text: String? = null
|
||||
) {
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = this::hideAlert,
|
||||
title = alertTitle(title),
|
||||
text = alertText(text),
|
||||
buttons = {
|
||||
Box(Modifier.fillMaxWidth().height(72.dp).padding(bottom = DEFAULT_PADDING * 2), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 3.dp)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showAlertMsg(
|
||||
title: StringResource,
|
||||
text: StringResource? = null,
|
||||
|
@ -12,6 +12,7 @@ import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.math.min
|
||||
|
||||
@Composable
|
||||
fun ModalView(
|
||||
@ -86,7 +87,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
|
||||
fun closeModal() {
|
||||
if (modalViews.isNotEmpty()) {
|
||||
if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex)
|
||||
else runAtomically { toRemove.add(modalViews.lastIndex - toRemove.size) }
|
||||
else runAtomically { toRemove.add(modalViews.lastIndex - min(toRemove.size, modalViews.lastIndex)) }
|
||||
}
|
||||
modalCount.value = modalViews.size - toRemove.size
|
||||
}
|
||||
|
@ -52,6 +52,14 @@ fun annotatedStringResource(id: StringResource): AnnotatedString {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedString {
|
||||
val density = LocalDensity.current
|
||||
return remember(id) {
|
||||
escapedHtmlToAnnotatedString(id.localized().format(args), density)
|
||||
}
|
||||
}
|
||||
|
||||
// maximum image file size to be auto-accepted
|
||||
const val MAX_IMAGE_SIZE: Long = 261_120 // 255KB
|
||||
const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2
|
||||
@ -67,7 +75,7 @@ const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB
|
||||
expect fun getAppFileUri(fileName: String): URI
|
||||
|
||||
// https://developer.android.com/training/data-storage/shared/documents-files#bitmap
|
||||
expect fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>?
|
||||
expect suspend fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>?
|
||||
|
||||
expect fun getFileName(uri: URI): String?
|
||||
|
||||
@ -106,7 +114,7 @@ fun saveImage(image: ImageBitmap, encrypted: Boolean): CryptoFile? {
|
||||
return try {
|
||||
val ext = if (image.hasAlpha()) "png" else "jpg"
|
||||
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
|
||||
val destFileName = generateNewFileName("IMG", ext)
|
||||
val destFileName = generateNewFileName("IMG", ext, File(getAppFilePath("")))
|
||||
val destFile = File(getAppFilePath(destFileName))
|
||||
if (encrypted) {
|
||||
val args = writeCryptoFile(destFile.absolutePath, dataResized.toByteArray())
|
||||
@ -124,6 +132,24 @@ fun saveImage(image: ImageBitmap, encrypted: Boolean): CryptoFile? {
|
||||
}
|
||||
}
|
||||
|
||||
fun desktopSaveImageInTmp(uri: URI): CryptoFile? {
|
||||
val image = getBitmapFromUri(uri) ?: return null
|
||||
return try {
|
||||
val ext = if (image.hasAlpha()) "png" else "jpg"
|
||||
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
|
||||
val destFileName = generateNewFileName("IMG", ext, tmpDir)
|
||||
val destFile = File(tmpDir, destFileName)
|
||||
val output = FileOutputStream(destFile)
|
||||
dataResized.writeTo(output)
|
||||
output.flush()
|
||||
output.close()
|
||||
CryptoFile.plain(destFile.absolutePath)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Util.kt desktopSaveImageInTmp error: ${e.stackTraceToString()}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? {
|
||||
return try {
|
||||
val filename = getFileName(uri)?.lowercase()
|
||||
@ -134,7 +160,7 @@ fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? {
|
||||
}
|
||||
// Just in case the image has a strange extension
|
||||
if (ext.length < 3 || ext.length > 4) ext = "gif"
|
||||
val destFileName = generateNewFileName("IMG", ext)
|
||||
val destFileName = generateNewFileName("IMG", ext, File(getAppFilePath("")))
|
||||
val destFile = File(getAppFilePath(destFileName))
|
||||
if (encrypted) {
|
||||
val args = writeCryptoFile(destFile.absolutePath, uri.inputStream()?.readBytes() ?: return null)
|
||||
@ -156,7 +182,7 @@ fun saveFileFromUri(uri: URI, encrypted: Boolean, withAlertOnException: Boolean
|
||||
val inputStream = uri.inputStream()
|
||||
val fileToSave = getFileName(uri)
|
||||
return if (inputStream != null && fileToSave != null) {
|
||||
val destFileName = uniqueCombine(fileToSave)
|
||||
val destFileName = uniqueCombine(fileToSave, File(getAppFilePath("")))
|
||||
val destFile = File(getAppFilePath(destFileName))
|
||||
if (encrypted) {
|
||||
createTmpFileAndDelete { tmpFile ->
|
||||
@ -193,21 +219,21 @@ fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T {
|
||||
}
|
||||
}
|
||||
|
||||
fun generateNewFileName(prefix: String, ext: String): String {
|
||||
fun generateNewFileName(prefix: String, ext: String, dir: File): String {
|
||||
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
|
||||
sdf.timeZone = TimeZone.getTimeZone("GMT")
|
||||
val timestamp = sdf.format(Date())
|
||||
return uniqueCombine("${prefix}_$timestamp.$ext")
|
||||
return uniqueCombine("${prefix}_$timestamp.$ext", dir)
|
||||
}
|
||||
|
||||
fun uniqueCombine(fileName: String): String {
|
||||
fun uniqueCombine(fileName: String, dir: File): String {
|
||||
val orig = File(fileName)
|
||||
val name = orig.nameWithoutExtension
|
||||
val ext = orig.extension
|
||||
fun tryCombine(n: Int): String {
|
||||
val suffix = if (n == 0) "" else "_$n"
|
||||
val f = "$name$suffix.$ext"
|
||||
return if (File(getAppFilePath(f)).exists()) tryCombine(n + 1) else f
|
||||
return if (File(dir, f).exists()) tryCombine(n + 1) else f
|
||||
}
|
||||
return tryCombine(0)
|
||||
}
|
||||
@ -347,7 +373,7 @@ inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
|
||||
fun UriHandler.openVerifiedSimplexUri(uri: String) {
|
||||
val URI = try { URI.create(uri) } catch (e: Exception) { null }
|
||||
if (URI != null) {
|
||||
connectIfOpenedViaUri(URI, ChatModel)
|
||||
connectIfOpenedViaUri(chatModel.remoteHostId, URI, ChatModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
|
||||
if (!displayName.isNullOrEmpty()) {
|
||||
profile = Profile(displayName = displayName, fullName = "")
|
||||
}
|
||||
val createdUser = m.controller.apiCreateActiveUser(profile, pastTimestamp = true)
|
||||
val createdUser = m.controller.apiCreateActiveUser(null, profile, pastTimestamp = true)
|
||||
m.currentUser.value = createdUser
|
||||
m.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
|
||||
if (createdUser != null) {
|
||||
|
@ -25,11 +25,13 @@ import chat.simplex.res.MR
|
||||
@Composable
|
||||
fun AddContactView(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
connReqInvitation: String,
|
||||
contactConnection: MutableState<PendingContactConnection?>
|
||||
) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
AddContactLayout(
|
||||
rhId = rhId,
|
||||
chatModel = chatModel,
|
||||
incognitoPref = chatModel.controller.appPrefs.incognito,
|
||||
connReq = connReqInvitation,
|
||||
@ -52,6 +54,7 @@ fun AddContactView(
|
||||
@Composable
|
||||
fun AddContactLayout(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
connReq: String,
|
||||
contactConnection: MutableState<PendingContactConnection?>,
|
||||
@ -63,9 +66,9 @@ fun AddContactLayout(
|
||||
withApi {
|
||||
val contactConnVal = contactConnection.value
|
||||
if (contactConnVal != null) {
|
||||
chatModel.controller.apiSetConnectionIncognito(contactConnVal.pccConnId, incognito.value)?.let {
|
||||
chatModel.controller.apiSetConnectionIncognito(rhId, contactConnVal.pccConnId, incognito.value)?.let {
|
||||
contactConnection.value = it
|
||||
chatModel.updateContactConnection(it)
|
||||
chatModel.updateContactConnection(rhId, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -172,6 +175,7 @@ fun sharedProfileInfo(
|
||||
fun PreviewAddContactView() {
|
||||
SimpleXTheme {
|
||||
AddContactLayout(
|
||||
rhId = null,
|
||||
chatModel = ChatModel,
|
||||
incognitoPref = SharedPreference({ false }, {}),
|
||||
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
|
||||
|
@ -32,25 +32,25 @@ import kotlinx.coroutines.launch
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
|
||||
fun AddGroupView(chatModel: ChatModel, rhId: Long?, close: () -> Unit) {
|
||||
AddGroupLayout(
|
||||
createGroup = { incognito, groupProfile ->
|
||||
withApi {
|
||||
val groupInfo = chatModel.controller.apiNewGroup(incognito, groupProfile)
|
||||
val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile)
|
||||
if (groupInfo != null) {
|
||||
chatModel.addChat(Chat(chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf()))
|
||||
chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf()))
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatModel.chatId.value = groupInfo.id
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
close.invoke()
|
||||
if (!groupInfo.incognito) {
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
AddGroupMembersView(groupInfo, creatingGroup = true, chatModel, close)
|
||||
AddGroupMembersView(rhId, groupInfo, creatingGroup = true, chatModel, close)
|
||||
}
|
||||
} else {
|
||||
ModalManager.end.showModalCloseable(true) { close ->
|
||||
GroupLinkView(chatModel, groupInfo, connReqContact = null, memberRole = null, onGroupLinkUpdated = null, creatingGroup = true, close)
|
||||
GroupLinkView(chatModel, rhId, groupInfo, connReqContact = null, memberRole = null, onGroupLinkUpdated = null, creatingGroup = true, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,4 +8,4 @@ enum class ConnectViaLinkTab {
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun ConnectViaLinkView(m: ChatModel, close: () -> Unit)
|
||||
expect fun ConnectViaLinkView(m: ChatModel, rhId: Long?, close: () -> Unit)
|
||||
|
@ -30,6 +30,7 @@ import chat.simplex.res.MR
|
||||
@Composable
|
||||
fun ContactConnectionInfoView(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
connReqInvitation: String?,
|
||||
contactConnection: PendingContactConnection,
|
||||
focusAlias: Boolean,
|
||||
@ -55,8 +56,8 @@ fun ContactConnectionInfoView(
|
||||
connReq = connReqInvitation,
|
||||
contactConnection = contactConnection,
|
||||
focusAlias = focusAlias,
|
||||
deleteConnection = { deleteContactConnectionAlert(contactConnection, chatModel, close) },
|
||||
onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) },
|
||||
deleteConnection = { deleteContactConnectionAlert(rhId, contactConnection, chatModel, close) },
|
||||
onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) },
|
||||
share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) },
|
||||
learnMore = {
|
||||
ModalManager.center.showModal {
|
||||
@ -165,9 +166,9 @@ fun DeleteButton(onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
private fun setContactAlias(contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withApi {
|
||||
chatModel.controller.apiSetConnectionAlias(contactConnection.pccConnId, localAlias)?.let {
|
||||
chatModel.updateContactConnection(it)
|
||||
private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withApi {
|
||||
chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let {
|
||||
chatModel.updateContactConnection(rhId, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ enum class CreateLinkTab {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
|
||||
fun CreateLinkView(m: ChatModel, rhId: Long?, initialSelection: CreateLinkTab) {
|
||||
val selection = remember { mutableStateOf(initialSelection) }
|
||||
val connReqInvitation = rememberSaveable { m.connReqInv }
|
||||
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(null) }
|
||||
@ -32,7 +32,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
|
||||
&& contactConnection.value == null
|
||||
&& !creatingConnReq.value
|
||||
) {
|
||||
createInvitation(m, creatingConnReq, connReqInvitation, contactConnection)
|
||||
createInvitation(m, rhId, creatingConnReq, connReqInvitation, contactConnection)
|
||||
}
|
||||
}
|
||||
/** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv].
|
||||
@ -65,10 +65,10 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
when (selection.value) {
|
||||
CreateLinkTab.ONE_TIME -> {
|
||||
AddContactView(m, connReqInvitation.value ?: "", contactConnection)
|
||||
AddContactView(m, rhId,connReqInvitation.value ?: "", contactConnection)
|
||||
}
|
||||
CreateLinkTab.LONG_TERM -> {
|
||||
UserAddressView(m, viaCreateLinkView = true, close = {})
|
||||
UserAddressView(m, rhId, viaCreateLinkView = true, close = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -100,13 +100,14 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
|
||||
|
||||
private fun createInvitation(
|
||||
m: ChatModel,
|
||||
rhId: Long?,
|
||||
creatingConnReq: MutableState<Boolean>,
|
||||
connReqInvitation: MutableState<String?>,
|
||||
contactConnection: MutableState<PendingContactConnection?>
|
||||
) {
|
||||
creatingConnReq.value = true
|
||||
withApi {
|
||||
val r = m.controller.apiAddContact(incognito = m.controller.appPrefs.incognito.get())
|
||||
val r = m.controller.apiAddContact(rhId, incognito = m.controller.appPrefs.incognito.get())
|
||||
if (r != null) {
|
||||
connReqInvitation.value = r.first
|
||||
contactConnection.value = r.second
|
||||
|
@ -33,7 +33,8 @@ import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
|
||||
fun NewChatSheet(chatModel: ChatModel, rhId: Long?, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
|
||||
// TODO close new chat if remote host changes in model
|
||||
if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) }
|
||||
NewChatSheetLayout(
|
||||
newChatSheetState,
|
||||
@ -41,17 +42,17 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView
|
||||
addContact = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModal { CreateLinkView(chatModel, CreateLinkTab.ONE_TIME) }
|
||||
ModalManager.center.showModal { CreateLinkView(chatModel, rhId, CreateLinkTab.ONE_TIME) }
|
||||
},
|
||||
connectViaLink = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showModalCloseable { close -> ConnectViaLinkView(chatModel, close) }
|
||||
ModalManager.center.showModalCloseable { close -> ConnectViaLinkView(chatModel, rhId, close) }
|
||||
},
|
||||
createGroup = {
|
||||
closeNewChatSheet(false)
|
||||
ModalManager.center.closeModals()
|
||||
ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, close) }
|
||||
ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, rhId, close) }
|
||||
},
|
||||
closeNewChatSheet,
|
||||
)
|
||||
|
@ -24,11 +24,12 @@ import chat.simplex.res.MR
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
|
||||
fun PasteToConnectView(chatModel: ChatModel, rhId: Long?, close: () -> Unit) {
|
||||
val connectionLink = remember { mutableStateOf("") }
|
||||
val clipboard = LocalClipboardManager.current
|
||||
PasteToConnectLayout(
|
||||
chatModel = chatModel,
|
||||
rhId = rhId,
|
||||
incognitoPref = chatModel.controller.appPrefs.incognito,
|
||||
connectionLink = connectionLink,
|
||||
pasteFromClipboard = {
|
||||
@ -41,6 +42,7 @@ fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
|
||||
@Composable
|
||||
fun PasteToConnectLayout(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
connectionLink: MutableState<String>,
|
||||
pasteFromClipboard: () -> Unit,
|
||||
@ -52,7 +54,7 @@ fun PasteToConnectLayout(
|
||||
try {
|
||||
val uri = URI(connReqUri)
|
||||
withApi {
|
||||
planAndConnect(chatModel, uri, incognito = incognito.value, close)
|
||||
planAndConnect(chatModel, rhId, uri, incognito = incognito.value, close)
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@ -124,6 +126,7 @@ fun PreviewPasteToConnectTextbox() {
|
||||
SimpleXTheme {
|
||||
PasteToConnectLayout(
|
||||
chatModel = ChatModel,
|
||||
rhId = null,
|
||||
incognitoPref = SharedPreference({ false }, {}),
|
||||
connectionLink = remember { mutableStateOf("") },
|
||||
pasteFromClipboard = {},
|
||||
|
@ -26,7 +26,7 @@ import chat.simplex.res.MR
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
expect fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit)
|
||||
expect fun ScanToConnectView(chatModel: ChatModel, rhId: Long?, close: () -> Unit)
|
||||
|
||||
enum class ConnectionLinkType {
|
||||
INVITATION, CONTACT, GROUP
|
||||
@ -34,21 +34,22 @@ enum class ConnectionLinkType {
|
||||
|
||||
suspend fun planAndConnect(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
incognito: Boolean?,
|
||||
close: (() -> Unit)?
|
||||
) {
|
||||
val connectionPlan = chatModel.controller.apiConnectPlan(uri.toString())
|
||||
val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri.toString())
|
||||
if (connectionPlan != null) {
|
||||
when (connectionPlan) {
|
||||
is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) {
|
||||
InvitationLinkPlan.Ok -> {
|
||||
Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito")
|
||||
if (incognito != null) {
|
||||
connectViaUri(chatModel, uri, incognito, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, uri, connectionPlan, close,
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_via_invitation_link),
|
||||
text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)),
|
||||
connectDestructive = false
|
||||
@ -62,12 +63,12 @@ suspend fun planAndConnect(
|
||||
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
|
||||
text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link),
|
||||
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
|
||||
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
|
||||
destructive = true,
|
||||
)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, uri, connectionPlan, close,
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
|
||||
text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link)),
|
||||
connectDestructive = true
|
||||
@ -78,7 +79,7 @@ suspend fun planAndConnect(
|
||||
Log.d(TAG, "planAndConnect, .InvitationLink, .Connecting, incognito=$incognito")
|
||||
val contact = connectionPlan.invitationLinkPlan.contact_
|
||||
if (contact != null) {
|
||||
openKnownContact(chatModel, close, contact)
|
||||
openKnownContact(chatModel, rhId, close, contact)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.contact_already_exists),
|
||||
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName)
|
||||
@ -93,7 +94,7 @@ suspend fun planAndConnect(
|
||||
is InvitationLinkPlan.Known -> {
|
||||
Log.d(TAG, "planAndConnect, .InvitationLink, .Known, incognito=$incognito")
|
||||
val contact = connectionPlan.invitationLinkPlan.contact
|
||||
openKnownContact(chatModel, close, contact)
|
||||
openKnownContact(chatModel, rhId, close, contact)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.contact_already_exists),
|
||||
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName)
|
||||
@ -104,10 +105,10 @@ suspend fun planAndConnect(
|
||||
ContactAddressPlan.Ok -> {
|
||||
Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito")
|
||||
if (incognito != null) {
|
||||
connectViaUri(chatModel, uri, incognito, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, uri, connectionPlan, close,
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_via_contact_link),
|
||||
text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)),
|
||||
connectDestructive = false
|
||||
@ -121,12 +122,12 @@ suspend fun planAndConnect(
|
||||
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
|
||||
text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address),
|
||||
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
|
||||
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
|
||||
destructive = true,
|
||||
)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, uri, connectionPlan, close,
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
|
||||
text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address)),
|
||||
connectDestructive = true
|
||||
@ -140,12 +141,12 @@ suspend fun planAndConnect(
|
||||
title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
|
||||
text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address),
|
||||
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
|
||||
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
|
||||
destructive = true,
|
||||
)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, uri, connectionPlan, close,
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
|
||||
text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address)),
|
||||
connectDestructive = true
|
||||
@ -155,7 +156,7 @@ suspend fun planAndConnect(
|
||||
is ContactAddressPlan.ConnectingProhibit -> {
|
||||
Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit, incognito=$incognito")
|
||||
val contact = connectionPlan.contactAddressPlan.contact
|
||||
openKnownContact(chatModel, close, contact)
|
||||
openKnownContact(chatModel, rhId, close, contact)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.contact_already_exists),
|
||||
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName)
|
||||
@ -164,7 +165,7 @@ suspend fun planAndConnect(
|
||||
is ContactAddressPlan.Known -> {
|
||||
Log.d(TAG, "planAndConnect, .ContactAddress, .Known, incognito=$incognito")
|
||||
val contact = connectionPlan.contactAddressPlan.contact
|
||||
openKnownContact(chatModel, close, contact)
|
||||
openKnownContact(chatModel, rhId, close, contact)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.contact_already_exists),
|
||||
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName)
|
||||
@ -175,9 +176,9 @@ suspend fun planAndConnect(
|
||||
val contact = connectionPlan.contactAddressPlan.contact
|
||||
if (incognito != null) {
|
||||
close?.invoke()
|
||||
connectContactViaAddress(chatModel, contact.contactId, incognito)
|
||||
connectContactViaAddress(chatModel, rhId, contact.contactId, incognito)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, contact, close, openChat = false)
|
||||
askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -189,11 +190,11 @@ suspend fun planAndConnect(
|
||||
title = generalGetString(MR.strings.connect_via_group_link),
|
||||
text = generalGetString(MR.strings.you_will_join_group),
|
||||
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
|
||||
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } }
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } }
|
||||
)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, uri, connectionPlan, close,
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_via_group_link),
|
||||
text = AnnotatedString(generalGetString(MR.strings.you_will_join_group)),
|
||||
connectDestructive = false
|
||||
@ -203,7 +204,7 @@ suspend fun planAndConnect(
|
||||
is GroupLinkPlan.OwnLink -> {
|
||||
Log.d(TAG, "planAndConnect, .GroupLink, .OwnLink, incognito=$incognito")
|
||||
val groupInfo = connectionPlan.groupLinkPlan.groupInfo
|
||||
ownGroupLinkConfirmConnect(chatModel, uri, incognito, connectionPlan, groupInfo, close)
|
||||
ownGroupLinkConfirmConnect(chatModel, rhId, uri, incognito, connectionPlan, groupInfo, close)
|
||||
}
|
||||
GroupLinkPlan.ConnectingConfirmReconnect -> {
|
||||
Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect, incognito=$incognito")
|
||||
@ -212,12 +213,12 @@ suspend fun planAndConnect(
|
||||
title = generalGetString(MR.strings.connect_plan_repeat_join_request),
|
||||
text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link),
|
||||
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
|
||||
onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } },
|
||||
onConfirm = { withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } },
|
||||
destructive = true,
|
||||
)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, uri, connectionPlan, close,
|
||||
chatModel, rhId, uri, connectionPlan, close,
|
||||
title = generalGetString(MR.strings.connect_plan_repeat_join_request),
|
||||
text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link)),
|
||||
connectDestructive = true
|
||||
@ -242,7 +243,7 @@ suspend fun planAndConnect(
|
||||
is GroupLinkPlan.Known -> {
|
||||
Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito")
|
||||
val groupInfo = connectionPlan.groupLinkPlan.groupInfo
|
||||
openKnownGroup(chatModel, close, groupInfo)
|
||||
openKnownGroup(chatModel, rhId, close, groupInfo)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.connect_plan_group_already_exists),
|
||||
String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName)
|
||||
@ -253,10 +254,10 @@ suspend fun planAndConnect(
|
||||
} else {
|
||||
Log.d(TAG, "planAndConnect, plan error")
|
||||
if (incognito != null) {
|
||||
connectViaUri(chatModel, uri, incognito, connectionPlan = null, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan = null, close)
|
||||
} else {
|
||||
askCurrentOrIncognitoProfileAlert(
|
||||
chatModel, uri, connectionPlan = null, close,
|
||||
chatModel, rhId, uri, connectionPlan = null, close,
|
||||
title = generalGetString(MR.strings.connect_plan_connect_via_link),
|
||||
connectDestructive = false
|
||||
)
|
||||
@ -266,12 +267,13 @@ suspend fun planAndConnect(
|
||||
|
||||
suspend fun connectViaUri(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
incognito: Boolean,
|
||||
connectionPlan: ConnectionPlan?,
|
||||
close: (() -> Unit)?
|
||||
): Boolean {
|
||||
val r = chatModel.controller.apiConnect(incognito, uri.toString())
|
||||
val r = chatModel.controller.apiConnect(rhId, incognito, uri.toString())
|
||||
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
|
||||
if (r) {
|
||||
close?.invoke()
|
||||
@ -298,6 +300,7 @@ fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType
|
||||
|
||||
fun askCurrentOrIncognitoProfileAlert(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
connectionPlan: ConnectionPlan?,
|
||||
close: (() -> Unit)?,
|
||||
@ -314,7 +317,7 @@ fun askCurrentOrIncognitoProfileAlert(
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
withApi {
|
||||
connectViaUri(chatModel, uri, incognito = false, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor)
|
||||
@ -322,7 +325,7 @@ fun askCurrentOrIncognitoProfileAlert(
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
withApi {
|
||||
connectViaUri(chatModel, uri, incognito = true, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor)
|
||||
@ -337,18 +340,19 @@ fun askCurrentOrIncognitoProfileAlert(
|
||||
)
|
||||
}
|
||||
|
||||
fun openKnownContact(chatModel: ChatModel, close: (() -> Unit)?, contact: Contact) {
|
||||
fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, contact: Contact) {
|
||||
withApi {
|
||||
val c = chatModel.getContactChat(contact.contactId)
|
||||
if (c != null) {
|
||||
close?.invoke()
|
||||
openDirectChat(contact.contactId, chatModel)
|
||||
openDirectChat(rhId, contact.contactId, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ownGroupLinkConfirmConnect(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
incognito: Boolean?,
|
||||
connectionPlan: ConnectionPlan?,
|
||||
@ -363,7 +367,7 @@ fun ownGroupLinkConfirmConnect(
|
||||
// Open group
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
openKnownGroup(chatModel, close, groupInfo)
|
||||
openKnownGroup(chatModel, rhId, close, groupInfo)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
@ -372,7 +376,7 @@ fun ownGroupLinkConfirmConnect(
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
withApi {
|
||||
connectViaUri(chatModel, uri, incognito, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close)
|
||||
}
|
||||
}) {
|
||||
Text(
|
||||
@ -385,7 +389,7 @@ fun ownGroupLinkConfirmConnect(
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
withApi {
|
||||
connectViaUri(chatModel, uri, incognito = false, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
@ -394,7 +398,7 @@ fun ownGroupLinkConfirmConnect(
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
withApi {
|
||||
connectViaUri(chatModel, uri, incognito = true, connectionPlan, close)
|
||||
connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close)
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
@ -411,12 +415,12 @@ fun ownGroupLinkConfirmConnect(
|
||||
)
|
||||
}
|
||||
|
||||
fun openKnownGroup(chatModel: ChatModel, close: (() -> Unit)?, groupInfo: GroupInfo) {
|
||||
fun openKnownGroup(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, groupInfo: GroupInfo) {
|
||||
withApi {
|
||||
val g = chatModel.getGroupChat(groupInfo.groupId)
|
||||
if (g != null) {
|
||||
close?.invoke()
|
||||
openGroupChat(groupInfo.groupId, chatModel)
|
||||
openGroupChat(rhId, groupInfo.groupId, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -424,6 +428,7 @@ fun openKnownGroup(chatModel: ChatModel, close: (() -> Unit)?, groupInfo: GroupI
|
||||
@Composable
|
||||
fun ConnectContactLayout(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
close: () -> Unit
|
||||
) {
|
||||
@ -435,7 +440,7 @@ fun ConnectContactLayout(
|
||||
try {
|
||||
val uri = URI(connReqUri)
|
||||
withApi {
|
||||
planAndConnect(chatModel, uri, incognito = incognito.value, close)
|
||||
planAndConnect(chatModel, rhId, uri, incognito = incognito.value, close)
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@ -487,6 +492,7 @@ fun PreviewConnectContactLayout() {
|
||||
SimpleXTheme {
|
||||
ConnectContactLayout(
|
||||
chatModel = ChatModel,
|
||||
rhId = null,
|
||||
incognitoPref = SharedPreference({ false }, {}),
|
||||
close = {},
|
||||
)
|
||||
|
@ -23,14 +23,14 @@ import chat.simplex.common.views.newchat.simplexChatLink
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun CreateSimpleXAddress(m: ChatModel) {
|
||||
fun CreateSimpleXAddress(m: ChatModel, rhId: Long?) {
|
||||
var progressIndicator by remember { mutableStateOf(false) }
|
||||
val userAddress = remember { m.userAddress }
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
prepareChatBeforeAddressCreation()
|
||||
prepareChatBeforeAddressCreation(rhId)
|
||||
}
|
||||
|
||||
CreateSimpleXAddressLayout(
|
||||
@ -45,11 +45,11 @@ fun CreateSimpleXAddress(m: ChatModel) {
|
||||
createAddress = {
|
||||
withApi {
|
||||
progressIndicator = true
|
||||
val connReqContact = m.controller.apiCreateUserAddress()
|
||||
val connReqContact = m.controller.apiCreateUserAddress(rhId)
|
||||
if (connReqContact != null) {
|
||||
m.userAddress.value = UserContactLinkRec(connReqContact)
|
||||
try {
|
||||
val u = m.controller.apiSetProfileAddress(true)
|
||||
val u = m.controller.apiSetProfileAddress(rhId, true)
|
||||
if (u != null) {
|
||||
m.updateUser(u)
|
||||
}
|
||||
@ -176,18 +176,18 @@ private fun ProgressIndicator() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareChatBeforeAddressCreation() {
|
||||
private fun prepareChatBeforeAddressCreation(rhId: Long?) {
|
||||
if (chatModel.users.isNotEmpty()) return
|
||||
withApi {
|
||||
val user = chatModel.controller.apiGetActiveUser() ?: return@withApi
|
||||
val user = chatModel.controller.apiGetActiveUser(rhId) ?: return@withApi
|
||||
chatModel.currentUser.value = user
|
||||
if (chatModel.users.isEmpty()) {
|
||||
chatModel.controller.startChat(user)
|
||||
} else {
|
||||
val users = chatModel.controller.listUsers()
|
||||
val users = chatModel.controller.listUsers(rhId)
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
chatModel.controller.getUserChatData()
|
||||
chatModel.controller.getUserChatData(rhId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
|
||||
onDispose {
|
||||
if (m.chatRunning.value != true) {
|
||||
withBGApi {
|
||||
val user = chatController.apiGetActiveUser()
|
||||
val user = chatController.apiGetActiveUser(null)
|
||||
if (user != null) {
|
||||
m.controller.startChat(user)
|
||||
}
|
||||
|
@ -0,0 +1,472 @@
|
||||
package chat.simplex.common.views.remote
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemViewLongClickable
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.switchToLocalSession
|
||||
import chat.simplex.common.model.ChatModel.connectedToRemote
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.QRCodeScanner
|
||||
import chat.simplex.common.views.usersettings.PreferenceToggle
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun ConnectDesktopView(close: () -> Unit) {
|
||||
val deviceName = remember { controller.appPrefs.deviceNameForRemoteAccess.state }
|
||||
val closeWithAlert = {
|
||||
if (!connectedToRemote()) {
|
||||
close()
|
||||
} else {
|
||||
showDisconnectDesktopAlert(close)
|
||||
}
|
||||
}
|
||||
ModalView(close = closeWithAlert) {
|
||||
ConnectDesktopLayout(
|
||||
deviceName = deviceName.value!!,
|
||||
)
|
||||
}
|
||||
val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE }
|
||||
DisposableEffect(Unit) {
|
||||
withBGApi {
|
||||
if (!ntfModeService) platform.androidServiceStart()
|
||||
}
|
||||
onDispose {
|
||||
if (!ntfModeService) platform.androidServiceSafeStop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectDesktopLayout(deviceName: String) {
|
||||
val sessionAddress = remember { mutableStateOf("") }
|
||||
val remoteCtrls = remember { mutableStateListOf<RemoteCtrlInfo>() }
|
||||
val session = remember { chatModel.remoteCtrlSession }.value
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
if (session != null) {
|
||||
when (session.sessionState) {
|
||||
is UIRemoteCtrlSessionState.Starting -> ConnectingDesktop(session, null)
|
||||
is UIRemoteCtrlSessionState.Connecting -> ConnectingDesktop(session, session.sessionState.remoteCtrl_)
|
||||
is UIRemoteCtrlSessionState.PendingConfirmation -> {
|
||||
if (controller.appPrefs.confirmRemoteSessions.get() || session.sessionState.remoteCtrl_ == null) {
|
||||
VerifySession(session, session.sessionState.remoteCtrl_, session.sessionCode!!, remoteCtrls)
|
||||
} else {
|
||||
ConnectingDesktop(session, session.sessionState.remoteCtrl_)
|
||||
LaunchedEffect(Unit) {
|
||||
verifyDesktopSessionCode(remoteCtrls, session.sessionCode!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is UIRemoteCtrlSessionState.Connected -> ActiveSession(session, session.sessionState.remoteCtrl)
|
||||
}
|
||||
} else {
|
||||
ConnectDesktop(deviceName, remoteCtrls, sessionAddress)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
setDeviceName(deviceName)
|
||||
updateRemoteCtrls(remoteCtrls)
|
||||
onDispose {
|
||||
if (chatModel.remoteCtrlSession.value != null) {
|
||||
disconnectDesktop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectDesktop(deviceName: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, sessionAddress: MutableState<String>) {
|
||||
AppBarTitle(stringResource(MR.strings.connect_to_desktop))
|
||||
SectionView(stringResource(MR.strings.this_device_name).uppercase()) {
|
||||
DevicesView(deviceName, remoteCtrls) {
|
||||
if (it != "") {
|
||||
setDeviceName(it)
|
||||
controller.appPrefs.deviceNameForRemoteAccess.set(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
ScanDesktopAddressView(sessionAddress)
|
||||
if (controller.appPrefs.developerTools.get()) {
|
||||
SectionSpacer()
|
||||
DesktopAddressView(sessionAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
|
||||
AppBarTitle(stringResource(MR.strings.connecting_to_desktop))
|
||||
SectionView(stringResource(MR.strings.connecting_to_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
CtrlDeviceNameText(session, rc)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
CtrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
if (session.sessionCode != null) {
|
||||
SectionSpacer()
|
||||
SectionView(stringResource(MR.strings.session_code).uppercase()) {
|
||||
SessionCodeText(session.sessionCode!!)
|
||||
}
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
DisconnectButton(::disconnectDesktop)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessCode: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
|
||||
AppBarTitle(stringResource(MR.strings.verify_connection))
|
||||
SectionView(stringResource(MR.strings.connected_to_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
CtrlDeviceNameText(session, rc)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
CtrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(MR.strings.verify_code_with_desktop).uppercase()) {
|
||||
SessionCodeText(sessCode)
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
SectionItemView({ verifyDesktopSessionCode(remoteCtrls, sessCode) }) {
|
||||
Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.confirm_verb), tint = MaterialTheme.colors.secondary)
|
||||
TextIconSpaced(false)
|
||||
Text(generalGetString(MR.strings.confirm_verb))
|
||||
}
|
||||
|
||||
SectionView {
|
||||
DisconnectButton(::disconnectDesktop)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CtrlDeviceNameText(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
|
||||
val newDesktop = annotatedStringResource(MR.strings.new_desktop)
|
||||
val text = remember(rc) {
|
||||
var t = AnnotatedString(rc?.deviceViewName ?: session.ctrlAppInfo.deviceName)
|
||||
if (rc == null) {
|
||||
t = t + AnnotatedString(" ") + newDesktop
|
||||
}
|
||||
t
|
||||
}
|
||||
Text(text)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CtrlDeviceVersionText(session: RemoteCtrlSession) {
|
||||
val thisDeviceVersion = annotatedStringResource(MR.strings.this_device_version, session.appVersion)
|
||||
val text = remember(session) {
|
||||
val v = AnnotatedString(session.ctrlAppInfo.appVersionRange.maxVersion)
|
||||
var t = AnnotatedString("v$v")
|
||||
if (v.text != session.appVersion) {
|
||||
t = t + AnnotatedString(" ") + thisDeviceVersion
|
||||
}
|
||||
t
|
||||
}
|
||||
Text(text)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo) {
|
||||
AppBarTitle(stringResource(MR.strings.connected_to_desktop))
|
||||
SectionView(stringResource(MR.strings.connected_desktop).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(rc.deviceViewName)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
CtrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
if (session.sessionCode != null) {
|
||||
SectionSpacer()
|
||||
SectionView(stringResource(MR.strings.session_code).uppercase()) {
|
||||
SessionCodeText(session.sessionCode!!)
|
||||
}
|
||||
}
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
DisconnectButton(::disconnectDesktop)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessionCodeText(code: String) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
code.substring(0, 23),
|
||||
Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp),
|
||||
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 16.sp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DevicesView(deviceName: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, updateDeviceName: (String) -> Unit) {
|
||||
DeviceNameField(deviceName) { updateDeviceName(it) }
|
||||
if (remoteCtrls.isNotEmpty()) {
|
||||
SectionItemView({ ModalManager.start.showModal { LinkedDesktopsView(remoteCtrls) } }) {
|
||||
Text(generalGetString(MR.strings.linked_desktops))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanDesktopAddressView(sessionAddress: MutableState<String>) {
|
||||
SectionView(stringResource(MR.strings.scan_qr_code_from_desktop).uppercase()) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
.padding(DEFAULT_PADDING)
|
||||
) {
|
||||
QRCodeScanner { text ->
|
||||
sessionAddress.value = text
|
||||
processDesktopQRCode(sessionAddress, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DesktopAddressView(sessionAddress: MutableState<String>) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SectionView(stringResource(MR.strings.desktop_address).uppercase()) {
|
||||
if (sessionAddress.value.isEmpty()) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_content_paste),
|
||||
stringResource(MR.strings.paste_desktop_address),
|
||||
disabled = !clipboard.hasText(),
|
||||
click = {
|
||||
sessionAddress.value = clipboard.getText()?.text ?: ""
|
||||
},
|
||||
)
|
||||
} else {
|
||||
Row(Modifier.padding(horizontal = DEFAULT_PADDING).fillMaxWidth()) {
|
||||
val state = remember {
|
||||
mutableStateOf(TextFieldValue(sessionAddress.value))
|
||||
}
|
||||
DefaultBasicTextField(
|
||||
Modifier.fillMaxWidth(),
|
||||
state,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
) {
|
||||
state.value = it
|
||||
}
|
||||
KeyChangeEffect(state.value.text) {
|
||||
if (state.value.text.isNotEmpty()) {
|
||||
sessionAddress.value = state.value.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_wifi),
|
||||
stringResource(MR.strings.connect_to_desktop),
|
||||
disabled = sessionAddress.value.isEmpty(),
|
||||
click = {
|
||||
connectDesktopAddress(sessionAddress, sessionAddress.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.linked_desktops))
|
||||
SectionView(stringResource(MR.strings.desktop_devices).uppercase()) {
|
||||
remoteCtrls.forEach { rc ->
|
||||
val showMenu = rememberSaveable { mutableStateOf(false) }
|
||||
SectionItemViewLongClickable(click = {}, longClick = { showMenu.value = true }) {
|
||||
RemoteCtrl(rc)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemAction(stringResource(MR.strings.delete_verb), painterResource(MR.images.ic_delete), color = Color.Red) {
|
||||
unlinkDesktop(remoteCtrls, rc)
|
||||
showMenu.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.linked_desktop_options).uppercase()) {
|
||||
PreferenceToggle(stringResource(MR.strings.verify_connections), remember { controller.appPrefs.confirmRemoteSessions.state }.value) {
|
||||
controller.appPrefs.confirmRemoteSessions.set(it)
|
||||
}
|
||||
PreferenceToggle(stringResource(MR.strings.discover_on_network), remember { controller.appPrefs.connectRemoteViaMulticast.state }.value && false) {
|
||||
controller.appPrefs.confirmRemoteSessions.set(it)
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RemoteCtrl(rc: RemoteCtrlInfo) {
|
||||
Text(rc.deviceViewName)
|
||||
}
|
||||
|
||||
private fun setDeviceName(name: String) {
|
||||
withBGApi {
|
||||
controller.setLocalDeviceName(name)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRemoteCtrls(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) {
|
||||
withBGApi {
|
||||
val res = controller.listRemoteCtrls()
|
||||
if (res != null) {
|
||||
remoteCtrls.clear()
|
||||
remoteCtrls.addAll(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processDesktopQRCode(sessionAddress: MutableState<String>, resp: String) {
|
||||
connectDesktopAddress(sessionAddress, resp)
|
||||
}
|
||||
|
||||
private fun connectDesktopAddress(sessionAddress: MutableState<String>, addr: String) {
|
||||
withBGApi {
|
||||
val res = controller.connectRemoteCtrl(desktopAddress = addr)
|
||||
if (res.first != null) {
|
||||
val (rc_, ctrlAppInfo, v) = res.first!!
|
||||
sessionAddress.value = ""
|
||||
chatModel.remoteCtrlSession.value = RemoteCtrlSession(
|
||||
ctrlAppInfo = ctrlAppInfo,
|
||||
appVersion = v,
|
||||
sessionState = UIRemoteCtrlSessionState.Connecting(remoteCtrl_ = rc_)
|
||||
)
|
||||
} else {
|
||||
val e = res.second ?: return@withBGApi
|
||||
when {
|
||||
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert()
|
||||
e.chatError is ChatError.ChatErrorChat && e.chatError.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert()
|
||||
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.chatError.remoteCtrlError.appVersion)
|
||||
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null)
|
||||
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert()
|
||||
else -> {
|
||||
val errMsg = "${e.responseType}: ${e.details}"
|
||||
Log.e(TAG, "bad response: $errMsg")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), errMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyDesktopSessionCode(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, sessCode: String) {
|
||||
withBGApi {
|
||||
val rc = controller.verifyRemoteCtrlSession(sessCode)
|
||||
if (rc != null) {
|
||||
chatModel.remoteCtrlSession.value = chatModel.remoteCtrlSession.value?.copy(sessionState = UIRemoteCtrlSessionState.Connected(remoteCtrl = rc, sessionCode = sessCode))
|
||||
}
|
||||
updateRemoteCtrls(remoteCtrls)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisconnectButton(onClick: () -> Unit) {
|
||||
SectionItemView(onClick) {
|
||||
Icon(painterResource(MR.images.ic_close), generalGetString(MR.strings.disconnect_remote_host), tint = MaterialTheme.colors.secondary)
|
||||
TextIconSpaced(false)
|
||||
Text(generalGetString(MR.strings.disconnect_remote_host))
|
||||
}
|
||||
}
|
||||
|
||||
private fun disconnectDesktop(close: (() -> Unit)? = null) {
|
||||
withBGApi {
|
||||
controller.stopRemoteCtrl()
|
||||
switchToLocalSession()
|
||||
close?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
private fun unlinkDesktop(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, rc: RemoteCtrlInfo) {
|
||||
withBGApi {
|
||||
controller.deleteRemoteCtrl(rc.remoteCtrlId)
|
||||
remoteCtrls.removeAll { it.remoteCtrlId == rc.remoteCtrlId }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnlinkDesktopAlert(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, rc: RemoteCtrlInfo) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.unlink_desktop_question),
|
||||
confirmText = generalGetString(MR.strings.unlink_desktop),
|
||||
destructive = true,
|
||||
onConfirm = {
|
||||
unlinkDesktop(remoteCtrls, rc)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDisconnectDesktopAlert(close: (() -> Unit)?) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.disconnect_desktop_question),
|
||||
text = generalGetString(MR.strings.only_one_device_can_work_at_the_same_time),
|
||||
confirmText = generalGetString(MR.strings.disconnect_remote_host),
|
||||
destructive = true,
|
||||
onConfirm = { disconnectDesktop(close) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun showBadInvitationErrorAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.bad_desktop_address),
|
||||
)
|
||||
}
|
||||
|
||||
private fun showBadVersionAlert(v: String?) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.desktop_incompatible_version),
|
||||
text = generalGetString(MR.strings.desktop_app_version_is_incompatible).format(v ?: "")
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDesktopDisconnectedErrorAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.desktop_connection_terminated),
|
||||
)
|
||||
}
|
@ -0,0 +1,359 @@
|
||||
package chat.simplex.common.views.remote
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemViewLongClickable
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import TextIconSpaced
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.newchat.QRCode
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun ConnectMobileView(
|
||||
m: ChatModel
|
||||
) {
|
||||
val connecting = rememberSaveable() { mutableStateOf(false) }
|
||||
val remoteHosts = remember { chatModel.remoteHosts }
|
||||
val deviceName = m.controller.appPrefs.deviceNameForRemoteAccess
|
||||
LaunchedEffect(Unit) {
|
||||
controller.reloadRemoteHosts()
|
||||
}
|
||||
ConnectMobileLayout(
|
||||
deviceName = remember { deviceName.state },
|
||||
remoteHosts = remoteHosts,
|
||||
connecting,
|
||||
connectedHost = remember { m.currentRemoteHost },
|
||||
updateDeviceName = {
|
||||
withBGApi {
|
||||
if (it != "") {
|
||||
m.controller.setLocalDeviceName(it)
|
||||
deviceName.set(it)
|
||||
}
|
||||
}
|
||||
},
|
||||
addMobileDevice = { showAddingMobileDevice(connecting) },
|
||||
connectMobileDevice = { connectMobileDevice(it, connecting) },
|
||||
connectDesktop = { withBGApi { chatController.switchUIRemoteHost(null) } },
|
||||
deleteHost = { host ->
|
||||
withBGApi {
|
||||
val success = controller.deleteRemoteHost(host.remoteHostId)
|
||||
if (success) {
|
||||
chatModel.remoteHosts.removeAll { it.remoteHostId == host.remoteHostId }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConnectMobileLayout(
|
||||
deviceName: State<String?>,
|
||||
remoteHosts: List<RemoteHostInfo>,
|
||||
connecting: MutableState<Boolean>,
|
||||
connectedHost: State<RemoteHostInfo?>,
|
||||
updateDeviceName: (String) -> Unit,
|
||||
addMobileDevice: () -> Unit,
|
||||
connectMobileDevice: (RemoteHostInfo) -> Unit,
|
||||
connectDesktop: () -> Unit,
|
||||
deleteHost: (RemoteHostInfo) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles))
|
||||
SectionView(generalGetString(MR.strings.this_device_name).uppercase()) {
|
||||
DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) }
|
||||
SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile))
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
}
|
||||
SectionView(stringResource(MR.strings.devices).uppercase()) {
|
||||
SettingsActionItemWithContent(text = stringResource(MR.strings.this_device), icon = painterResource(MR.images.ic_desktop), click = connectDesktop) {
|
||||
if (connectedHost.value == null) {
|
||||
Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
|
||||
for (host in remoteHosts) {
|
||||
val showMenu = rememberSaveable { mutableStateOf(false) }
|
||||
SectionItemViewLongClickable({ connectMobileDevice(host) }, { showMenu.value = true }, disabled = connecting.value) {
|
||||
Icon(painterResource(MR.images.ic_smartphone_300), host.hostDeviceName, tint = MaterialTheme.colors.secondary)
|
||||
TextIconSpaced(false)
|
||||
Text(host.hostDeviceName)
|
||||
Spacer(Modifier.weight(1f))
|
||||
if (host.activeHost) {
|
||||
Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
} else if (host.sessionState is RemoteHostSessionState.Connected) {
|
||||
HostDisconnectButton { stopRemoteHostAndReloadHosts(host, false) }
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (host.activeHost) {
|
||||
ItemAction(stringResource(MR.strings.disconnect_remote_host), painterResource(MR.images.ic_wifi_off), color = WarningOrange) {
|
||||
stopRemoteHostAndReloadHosts(host, true)
|
||||
showMenu.value = false
|
||||
}
|
||||
} else {
|
||||
ItemAction(stringResource(MR.strings.delete_verb), painterResource(MR.images.ic_delete), color = Color.Red) {
|
||||
deleteHost(host)
|
||||
showMenu.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionItemView(addMobileDevice) {
|
||||
Icon(painterResource(MR.images.ic_add), stringResource(MR.strings.link_a_mobile), tint = MaterialTheme.colors.primary)
|
||||
Spacer(Modifier.padding(horizontal = 10.dp))
|
||||
Text(stringResource(MR.strings.link_a_mobile), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeviceNameField(
|
||||
initialValue: String,
|
||||
onChange: (String) -> Unit
|
||||
) {
|
||||
// TODO get user-defined device name
|
||||
val state = remember { mutableStateOf(TextFieldValue(initialValue)) }
|
||||
DefaultConfigurableTextField(
|
||||
state = state,
|
||||
placeholder = generalGetString(MR.strings.enter_this_device_name),
|
||||
modifier = Modifier.padding(start = DEFAULT_PADDING),
|
||||
isValid = { true },
|
||||
)
|
||||
KeyChangeEffect(state.value) {
|
||||
onChange(state.value.text)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConnectMobileViewLayout(
|
||||
title: String,
|
||||
invitation: String?,
|
||||
deviceName: String?,
|
||||
sessionCode: String?
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
AppBarTitle(title)
|
||||
SectionView {
|
||||
if (invitation != null && sessionCode == null) {
|
||||
QRCode(
|
||||
invitation, Modifier
|
||||
.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))
|
||||
|
||||
if (remember { controller.appPrefs.developerTools.state }.value) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
SectionItemView({ clipboard.shareText(invitation) }) {
|
||||
Text(generalGetString(MR.strings.share_link), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
}
|
||||
if (deviceName != null || sessionCode != null) {
|
||||
SectionView(stringResource(MR.strings.connected_mobile).uppercase()) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
deviceName ?: stringResource(MR.strings.new_mobile_device),
|
||||
Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp),
|
||||
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 16.sp, fontStyle = if (deviceName != null) FontStyle.Normal else FontStyle.Italic)
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
}
|
||||
|
||||
if (sessionCode != null) {
|
||||
SectionView(stringResource(MR.strings.verify_code_on_mobile).uppercase()) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
sessionCode.substring(0, 23),
|
||||
Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp),
|
||||
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 16.sp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun connectMobileDevice(rh: RemoteHostInfo, connecting: MutableState<Boolean>) {
|
||||
if (!rh.activeHost() && rh.sessionState is RemoteHostSessionState.Connected) {
|
||||
withBGApi {
|
||||
controller.switchUIRemoteHost(rh.remoteHostId)
|
||||
}
|
||||
} else if (rh.activeHost()) {
|
||||
showConnectedMobileDevice(rh) {
|
||||
stopRemoteHostAndReloadHosts(rh, true)
|
||||
}
|
||||
} else {
|
||||
showConnectMobileDevice(rh, connecting)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAddingMobileDevice(connecting: MutableState<Boolean>) {
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
val invitation = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val pairing = remember { chatModel.newRemoteHostPairing }
|
||||
val sessionCode = when (val state = pairing.value?.second) {
|
||||
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
|
||||
else -> null
|
||||
}
|
||||
/** 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 (cachedSessionCode == null) stringResource(MR.strings.link_a_mobile) else stringResource(MR.strings.verify_connection),
|
||||
invitation = invitation.value,
|
||||
deviceName = remoteDeviceName,
|
||||
sessionCode = cachedSessionCode
|
||||
)
|
||||
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 {
|
||||
val r = chatModel.controller.startRemoteHost(null)
|
||||
if (r != null) {
|
||||
connecting.value = true
|
||||
invitation.value = r.second
|
||||
}
|
||||
}
|
||||
onDispose {
|
||||
if (chatModel.currentRemoteHost.value?.remoteHostId == oldRemoteHostId) {
|
||||
withBGApi {
|
||||
chatController.stopRemoteHost(null)
|
||||
}
|
||||
}
|
||||
chatModel.newRemoteHostPairing.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState<Boolean>) {
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
val pairing = remember { chatModel.newRemoteHostPairing }
|
||||
val invitation = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val sessionCode = when (val state = pairing.value?.second) {
|
||||
is RemoteHostSessionState.PendingConfirmation -> state.sessionCode
|
||||
else -> null
|
||||
}
|
||||
/** 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
|
||||
}
|
||||
ConnectMobileViewLayout(
|
||||
title = if (cachedSessionCode == null) stringResource(MR.strings.scan_from_mobile) else stringResource(MR.strings.verify_connection),
|
||||
invitation = invitation.value,
|
||||
deviceName = pairing.value?.first?.hostDeviceName ?: rh.hostDeviceName,
|
||||
sessionCode = cachedSessionCode,
|
||||
)
|
||||
var remoteHostId by rememberSaveable { mutableStateOf<Long?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
val r = chatModel.controller.startRemoteHost(rh.remoteHostId)
|
||||
if (r != null) {
|
||||
val (rh_, inv) = r
|
||||
connecting.value = true
|
||||
remoteHostId = rh_?.remoteHostId
|
||||
invitation.value = inv
|
||||
}
|
||||
}
|
||||
LaunchedEffect(remember { chatModel.currentRemoteHost }.value) {
|
||||
if (remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId == remoteHostId) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
KeyChangeEffect(pairing.value) {
|
||||
if (pairing.value == null) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (remoteHostId != null && chatModel.currentRemoteHost.value?.remoteHostId != remoteHostId) {
|
||||
withBGApi {
|
||||
chatController.stopRemoteHost(remoteHostId)
|
||||
}
|
||||
}
|
||||
chatModel.newRemoteHostPairing.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showConnectedMobileDevice(rh: RemoteHostInfo, disconnectHost: () -> Unit) {
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
val sessionCode = when (val state = rh.sessionState) {
|
||||
is RemoteHostSessionState.Connected -> state.sessionCode
|
||||
else -> null
|
||||
}
|
||||
Column {
|
||||
ConnectMobileViewLayout(
|
||||
title = stringResource(MR.strings.connected_to_mobile),
|
||||
invitation = null,
|
||||
deviceName = rh.hostDeviceName,
|
||||
sessionCode = sessionCode
|
||||
)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
SectionItemView(disconnectHost) {
|
||||
Text(generalGetString(MR.strings.disconnect_remote_host), Modifier.fillMaxWidth(), color = WarningOrange)
|
||||
}
|
||||
}
|
||||
KeyChangeEffect(remember { chatModel.currentRemoteHost }.value) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@ fun HiddenProfileView(
|
||||
saveProfilePassword = { hidePassword ->
|
||||
withBGApi {
|
||||
try {
|
||||
val u = m.controller.apiHideUser(user.userId, hidePassword)
|
||||
val u = m.controller.apiHideUser(user, hidePassword)
|
||||
m.updateUser(u)
|
||||
close()
|
||||
} catch (e: Exception) {
|
||||
|
@ -168,9 +168,9 @@ fun NetworkAndServersView(
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.network_and_servers))
|
||||
SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.SMP, close) })
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) })
|
||||
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.XFTP, close) })
|
||||
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) })
|
||||
|
||||
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal)
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
|
||||
|
@ -25,11 +25,11 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
|
||||
fun savePrefs(afterSave: () -> Unit = {}) {
|
||||
withApi {
|
||||
val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences())
|
||||
val updated = m.controller.apiUpdateProfile(newProfile)
|
||||
val updated = m.controller.apiUpdateProfile(user.remoteHostId, newProfile)
|
||||
if (updated != null) {
|
||||
val (updatedProfile, updatedContacts) = updated
|
||||
m.updateCurrentUser(updatedProfile, preferences)
|
||||
updatedContacts.forEach(m::updateContact)
|
||||
m.updateCurrentUser(user.remoteHostId, updatedProfile, preferences)
|
||||
updatedContacts.forEach { m.updateContact(user.remoteHostId, it) }
|
||||
currentPreferences = preferences
|
||||
}
|
||||
afterSave()
|
||||
|
@ -99,7 +99,7 @@ fun PrivacySettingsView(
|
||||
fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) {
|
||||
withApi {
|
||||
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
|
||||
chatModel.controller.apiSetUserContactReceipts(currentUser.userId, mrs)
|
||||
chatModel.controller.apiSetUserContactReceipts(currentUser, mrs)
|
||||
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||
chatModel.currentUser.value = currentUser.copy(sendRcptsContacts = enable)
|
||||
if (clearOverrides) {
|
||||
@ -111,7 +111,7 @@ fun PrivacySettingsView(
|
||||
val sendRcpts = contact.chatSettings.sendRcpts
|
||||
if (sendRcpts != null && sendRcpts != enable) {
|
||||
contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null))
|
||||
chatModel.updateContact(contact)
|
||||
chatModel.updateContact(currentUser.remoteHostId, contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,7 +122,7 @@ fun PrivacySettingsView(
|
||||
fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) {
|
||||
withApi {
|
||||
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
|
||||
chatModel.controller.apiSetUserGroupReceipts(currentUser.userId, mrs)
|
||||
chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs)
|
||||
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||
chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable)
|
||||
if (clearOverrides) {
|
||||
@ -134,7 +134,7 @@ fun PrivacySettingsView(
|
||||
val sendRcpts = groupInfo.chatSettings.sendRcpts
|
||||
if (sendRcpts != null && sendRcpts != enable) {
|
||||
groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null))
|
||||
chatModel.updateGroup(groupInfo)
|
||||
chatModel.updateGroup(currentUser.remoteHostId, groupInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -197,7 +197,7 @@ fun ShowTestStatus(server: ServerCfg, modifier: Modifier = Modifier) =
|
||||
|
||||
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, ProtocolTestFailure?> =
|
||||
try {
|
||||
val r = m.controller.testProtoServer(server.server)
|
||||
val r = m.controller.testProtoServer(server.remoteHostId, server.server)
|
||||
server.copy(tested = r == null) to r
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "testServerConnection ${e.stackTraceToString()}")
|
||||
|
@ -28,7 +28,8 @@ import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: () -> Unit) {
|
||||
fun ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) {
|
||||
// TODO close if remote host changes
|
||||
var presetServers by remember { mutableStateOf(emptyList<String>()) }
|
||||
var servers by remember {
|
||||
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
|
||||
@ -51,7 +52,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val res = m.controller.getUserProtoServers(serverProtocol)
|
||||
val res = m.controller.getUserProtoServers(rhId, serverProtocol)
|
||||
if (res != null) {
|
||||
currServers.value = res.protoServers
|
||||
presetServers = res.presetServers
|
||||
@ -90,7 +91,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
|
||||
ModalView(
|
||||
close = {
|
||||
if (saveDisabled.value) close()
|
||||
else showUnsavedChangesAlert({ saveServers(serverProtocol, currServers, servers, m, close) }, close)
|
||||
else showUnsavedChangesAlert({ saveServers(rhId, serverProtocol, currServers, servers, m, close) }, close)
|
||||
},
|
||||
) {
|
||||
ProtocolServersLayout(
|
||||
@ -118,7 +119,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
ModalManager.start.showModalCloseable { close ->
|
||||
ScanProtocolServer {
|
||||
ScanProtocolServer(rhId) {
|
||||
close()
|
||||
servers = servers + it
|
||||
m.userSMPServersUnsaved.value = servers
|
||||
@ -133,7 +134,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
|
||||
if (!hasAllPresets) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
servers = (servers + addAllPresets(presetServers, servers, m)).sortedByDescending { it.preset }
|
||||
servers = (servers + addAllPresets(rhId, presetServers, servers, m)).sortedByDescending { it.preset }
|
||||
}) {
|
||||
Text(stringResource(MR.strings.smp_servers_preset_add), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
@ -155,7 +156,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
|
||||
m.userSMPServersUnsaved.value = null
|
||||
},
|
||||
saveSMPServers = {
|
||||
saveServers(serverProtocol, currServers, servers, m)
|
||||
saveServers(rhId, serverProtocol, currServers, servers, m)
|
||||
},
|
||||
showServer = ::showServer,
|
||||
)
|
||||
@ -289,11 +290,11 @@ private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List<Se
|
||||
private fun hasAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): Boolean =
|
||||
presetServers.all { hasPreset(it, servers) } ?: true
|
||||
|
||||
private fun addAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
|
||||
private fun addAllPresets(rhId: Long?, presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
|
||||
val toAdd = ArrayList<ServerCfg>()
|
||||
for (srv in presetServers) {
|
||||
if (!hasPreset(srv, servers)) {
|
||||
toAdd.add(ServerCfg(srv, preset = true, tested = null, enabled = true))
|
||||
toAdd.add(ServerCfg(remoteHostId = rhId, srv, preset = true, tested = null, enabled = true))
|
||||
}
|
||||
}
|
||||
return toAdd
|
||||
@ -346,9 +347,9 @@ private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpd
|
||||
return fs
|
||||
}
|
||||
|
||||
private fun saveServers(protocol: ServerProtocol, currServers: MutableState<List<ServerCfg>>, servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
|
||||
private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: MutableState<List<ServerCfg>>, servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
|
||||
withApi {
|
||||
if (m.controller.setUserProtoServers(protocol, servers)) {
|
||||
if (m.controller.setUserProtoServers(rhId, protocol, servers)) {
|
||||
currServers.value = servers
|
||||
m.userSMPServersUnsaved.value = null
|
||||
}
|
||||
|
@ -13,10 +13,10 @@ import chat.simplex.common.views.newchat.QRCodeScanner
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
expect fun ScanProtocolServer(onNext: (ServerCfg) -> Unit)
|
||||
expect fun ScanProtocolServer(rhId: Long?, onNext: (ServerCfg) -> Unit)
|
||||
|
||||
@Composable
|
||||
fun ScanProtocolServerLayout(onNext: (ServerCfg) -> Unit) {
|
||||
fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@ -32,7 +32,7 @@ fun ScanProtocolServerLayout(onNext: (ServerCfg) -> Unit) {
|
||||
QRCodeScanner { text ->
|
||||
val res = parseServerAddress(text)
|
||||
if (res != null) {
|
||||
onNext(ServerCfg(text, false, null, true))
|
||||
onNext(ServerCfg(remoteHostId = rhId, text, false, null, true))
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.smp_servers_invalid_address),
|
||||
|
@ -26,12 +26,12 @@ fun SetDeliveryReceiptsView(m: ChatModel) {
|
||||
if (currentUser != null) {
|
||||
withApi {
|
||||
try {
|
||||
m.controller.apiSetAllContactReceipts(enable = true)
|
||||
m.controller.apiSetAllContactReceipts(currentUser.remoteHostId, enable = true)
|
||||
m.currentUser.value = currentUser.copy(sendRcptsContacts = true)
|
||||
m.setDeliveryReceipts.value = false
|
||||
m.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
|
||||
try {
|
||||
val users = m.controller.listUsers()
|
||||
val users = m.controller.listUsers(currentUser.remoteHostId)
|
||||
m.users.clear()
|
||||
m.users.addAll(users)
|
||||
} catch (e: Exception) {
|
||||
|
@ -29,6 +29,8 @@ import chat.simplex.common.views.database.DatabaseView
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.onboarding.SimpleXInfo
|
||||
import chat.simplex.common.views.onboarding.WhatsNewView
|
||||
import chat.simplex.common.views.remote.ConnectDesktopView
|
||||
import chat.simplex.common.views.remote.ConnectMobileView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ -153,8 +155,13 @@ fun SettingsLayout(
|
||||
}
|
||||
val profileHidden = rememberSaveable { mutableStateOf(false) }
|
||||
SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true)
|
||||
SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, it.currentUser.value?.remoteHostId, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true)
|
||||
ChatPreferencesItem(showCustomModal, stopped = stopped)
|
||||
if (appPlatform.isDesktop) {
|
||||
SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView(it) }, disabled = stopped, extraPadding = true)
|
||||
} else {
|
||||
SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true)
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
|
@ -33,10 +33,12 @@ import chat.simplex.res.MR
|
||||
@Composable
|
||||
fun UserAddressView(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
viaCreateLinkView: Boolean = false,
|
||||
shareViaProfile: Boolean = false,
|
||||
close: () -> Unit
|
||||
) {
|
||||
// TODO close when remote host changes
|
||||
val shareViaProfile = remember { mutableStateOf(shareViaProfile) }
|
||||
var progressIndicator by remember { mutableStateOf(false) }
|
||||
val onCloseHandler: MutableState<(close: () -> Unit) -> Unit> = remember { mutableStateOf({ _ -> }) }
|
||||
@ -45,7 +47,7 @@ fun UserAddressView(
|
||||
progressIndicator = true
|
||||
withBGApi {
|
||||
try {
|
||||
val u = chatModel.controller.apiSetProfileAddress(on)
|
||||
val u = chatModel.controller.apiSetProfileAddress(rhId, on)
|
||||
if (u != null) {
|
||||
chatModel.updateUser(u)
|
||||
}
|
||||
@ -67,7 +69,7 @@ fun UserAddressView(
|
||||
createAddress = {
|
||||
withApi {
|
||||
progressIndicator = true
|
||||
val connReqContact = chatModel.controller.apiCreateUserAddress()
|
||||
val connReqContact = chatModel.controller.apiCreateUserAddress(rhId)
|
||||
if (connReqContact != null) {
|
||||
chatModel.userAddress.value = UserContactLinkRec(connReqContact)
|
||||
|
||||
@ -112,7 +114,7 @@ fun UserAddressView(
|
||||
onConfirm = {
|
||||
progressIndicator = true
|
||||
withApi {
|
||||
val u = chatModel.controller.apiDeleteUserAddress()
|
||||
val u = chatModel.controller.apiDeleteUserAddress(rhId)
|
||||
if (u != null) {
|
||||
chatModel.userAddress.value = null
|
||||
chatModel.updateUser(u)
|
||||
@ -126,7 +128,7 @@ fun UserAddressView(
|
||||
},
|
||||
saveAas = { aas: AutoAcceptState, savedAAS: MutableState<AutoAcceptState> ->
|
||||
withBGApi {
|
||||
val address = chatModel.controller.userAddressAutoAccept(aas.autoAccept)
|
||||
val address = chatModel.controller.userAddressAutoAccept(rhId, aas.autoAccept)
|
||||
if (address != null) {
|
||||
chatModel.userAddress.value = address
|
||||
savedAAS.value = aas
|
||||
|
@ -37,10 +37,10 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
|
||||
close,
|
||||
saveProfile = { displayName, fullName, image ->
|
||||
withApi {
|
||||
val updated = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName.trim(), fullName = fullName, image = image))
|
||||
val updated = chatModel.controller.apiUpdateProfile(user.remoteHostId, profile.copy(displayName = displayName.trim(), fullName = fullName, image = image))
|
||||
if (updated != null) {
|
||||
val (newProfile, _) = updated
|
||||
chatModel.updateCurrentUser(newProfile)
|
||||
chatModel.updateCurrentUser(user.remoteHostId, newProfile)
|
||||
profile = newProfile
|
||||
close()
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
withBGApi {
|
||||
m.controller.changeActiveUser(user.userId, userViewPassword(user, searchTextOrPassword.value.trim()))
|
||||
m.controller.changeActiveUser(user.remoteHostId, user.userId, userViewPassword(user, searchTextOrPassword.value.trim()))
|
||||
}
|
||||
},
|
||||
removeUser = { user ->
|
||||
@ -106,24 +106,24 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
|
||||
ModalManager.start.showModalCloseable(true) { close ->
|
||||
ProfileActionView(UserProfileAction.UNHIDE, user) { pwd ->
|
||||
withBGApi {
|
||||
setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, pwd) }
|
||||
setUserPrivacy(m) { m.controller.apiUnhideUser(user, pwd) }
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
withBGApi { setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, searchTextOrPassword.value.trim()) } }
|
||||
withBGApi { setUserPrivacy(m) { m.controller.apiUnhideUser(user, searchTextOrPassword.value.trim()) } }
|
||||
}
|
||||
},
|
||||
muteUser = { user ->
|
||||
withBGApi {
|
||||
setUserPrivacy(m, onSuccess = {
|
||||
if (m.controller.appPrefs.showMuteProfileAlert.get()) showMuteProfileAlert(m.controller.appPrefs.showMuteProfileAlert)
|
||||
}) { m.controller.apiMuteUser(user.userId) }
|
||||
}) { m.controller.apiMuteUser(user) }
|
||||
}
|
||||
},
|
||||
unmuteUser = { user ->
|
||||
withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user.userId) } }
|
||||
withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user) } }
|
||||
},
|
||||
showHiddenProfile = { user ->
|
||||
ModalManager.start.showModalCloseable(true) { close ->
|
||||
@ -348,14 +348,14 @@ private suspend fun doRemoveUser(m: ChatModel, user: User, users: List<User>, de
|
||||
if (users.size < 2) return
|
||||
|
||||
suspend fun deleteUser(user: User) {
|
||||
m.controller.apiDeleteUser(user.userId, delSMPQueues, viewPwd)
|
||||
m.controller.apiDeleteUser(user, delSMPQueues, viewPwd)
|
||||
m.removeUser(user)
|
||||
}
|
||||
try {
|
||||
if (user.activeUser) {
|
||||
val newActive = users.firstOrNull { u -> !u.activeUser && !u.hidden }
|
||||
if (newActive != null) {
|
||||
m.controller.changeActiveUser_(newActive.userId, null)
|
||||
m.controller.changeActiveUser_(newActive.remoteHostId, newActive.userId, null)
|
||||
deleteUser(user.copy(activeUser = false))
|
||||
}
|
||||
} else {
|
||||
|
@ -350,6 +350,8 @@
|
||||
<string name="file_saved">File saved</string>
|
||||
<string name="file_not_found">File not found</string>
|
||||
<string name="error_saving_file">Error saving file</string>
|
||||
<string name="loading_remote_file_title">Loading the file </string>
|
||||
<string name="loading_remote_file_desc">Please, wait while the file is being loaded from the linked mobile</string>
|
||||
|
||||
<!-- Voice messages -->
|
||||
<string name="voice_message">Voice message</string>
|
||||
@ -952,6 +954,7 @@
|
||||
<string name="settings_section_title_calls">CALLS</string>
|
||||
<string name="settings_section_title_incognito">Incognito mode</string>
|
||||
<string name="settings_section_title_experimenta">EXPERIMENTAL</string>
|
||||
<string name="settings_section_title_use_from_desktop">Use from desktop</string>
|
||||
|
||||
<!-- DatabaseView.kt -->
|
||||
<string name="your_chat_database">Your chat database</string>
|
||||
@ -1627,6 +1630,49 @@
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">You can enable them later via app Privacy & Security settings.</string>
|
||||
<string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string>
|
||||
|
||||
<!-- Remote access -->
|
||||
<string name="link_a_mobile">Link a mobile</string>
|
||||
<string name="linked_mobiles">Linked mobiles</string>
|
||||
<string name="scan_from_mobile">Scan from mobile</string>
|
||||
<string name="verify_connection">Verify connection</string>
|
||||
<string name="verify_code_on_mobile">Verify code on mobile</string>
|
||||
<string name="this_device_name">This device name</string>
|
||||
<string name="this_device_version"><![CDATA[<i>(this device v%s)</i>]]></string>
|
||||
<string name="connected_mobile">Connected mobile</string>
|
||||
<string name="connected_to_mobile">Connected to mobile</string>
|
||||
<string name="enter_this_device_name">Enter this device name…</string>
|
||||
<string name="this_device_name_shared_with_mobile">The device name will be shared with the connected mobile client.</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="this_device">This device</string>
|
||||
<string name="devices">Devices</string>
|
||||
<string name="new_mobile_device">New mobile device</string>
|
||||
<string name="unlink_desktop_question">Unlink desktop?</string>
|
||||
<string name="unlink_desktop">Unlink</string>
|
||||
<string name="disconnect_remote_host">Disconnect</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="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code]]></string>
|
||||
<string name="bad_desktop_address">Bad desktop address</string>
|
||||
<string name="desktop_incompatible_version">Incompatible version</string>
|
||||
<string name="desktop_app_version_is_incompatible">Desktop app version %s is not compatible with this app.</string>
|
||||
<string name="desktop_connection_terminated">Connection terminated</string>
|
||||
<string name="session_code">Session code</string>
|
||||
<string name="connecting_to_desktop">Connecting to desktop</string>
|
||||
<string name="connect_to_desktop">Connect to desktop</string>
|
||||
<string name="connected_to_desktop">Connected to desktop</string>
|
||||
<string name="connected_desktop">Connected desktop</string>
|
||||
<string name="verify_code_with_desktop">Verify code with desktop</string>
|
||||
<string name="new_desktop"><![CDATA[<i>(new)</i>]]></string>
|
||||
<string name="linked_desktops">Linked desktops</string>
|
||||
<string name="desktop_devices">Desktop devices</string>
|
||||
<string name="linked_desktop_options">Linked desktop options</string>
|
||||
<string name="scan_qr_code_from_desktop">Scan QR code from desktop</string>
|
||||
<string name="desktop_address">Desktop address</string>
|
||||
<string name="verify_connections">Verify connections</string>
|
||||
<string name="discover_on_network">Discover on network</string>
|
||||
<string name="paste_desktop_address">Paste desktop address</string>
|
||||
<string name="desktop_device">Desktop</string>
|
||||
|
||||
<!-- Under development -->
|
||||
<string name="in_developing_title">Coming soon!</string>
|
||||
<string name="in_developing_desc">This feature is not yet supported. Try the next release.</string>
|
||||
|
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M261.5-45q-22.969 0-40.234-17.266Q204-79.53 204-102.5v-755q0-22.969 17.266-40.234Q238.531-915 261.5-915h437q22.969 0 40.234 17.266Q756-880.469 756-857.5v755q0 22.969-17.266 40.234Q721.469-45 698.5-45h-437Zm0-88.5v31h437v-31h-437Zm0-57.5h437v-578h-437v578Zm0-635.5h437v-31h-437v31Zm0 0v-31 31Zm0 693v31-31Z"/></svg>
|
After Width: | Height: | Size: 411 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M278.533-63.078q-22.606 0-39.338-16.732t-16.732-39.327v-721.726q0-22.595 16.732-39.327t39.338-16.732h402.934q22.606 0 39.338 16.732t16.732 39.327v721.726q0 22.595-16.732 39.327t-39.338 16.732H278.533Zm-12.225-87.154v31q0 4.616 3.846 8.462 3.847 3.847 8.463 3.847h402.766q4.616 0 8.463-3.847 3.846-3.846 3.846-8.462v-31H266.308Zm0-43.845h427.384v-571.846H266.308v571.846Zm0-615.691h427.384v-31q0-4.616-3.846-8.462-3.847-3.847-8.463-3.847H278.617q-4.616 0-8.463 3.847-3.846 3.846-3.846 8.462v31Zm0 0V-853.077v43.309Zm0 659.536V-106.923v-43.309Z"/></svg>
|
After Width: | Height: | Size: 648 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M479.965-133.5q-34.965 0-61.215-26.295-26.25-26.296-26.25-61.275 0-34.43 26.285-60.93t61.25-26.5q34.965 0 61.215 26.57 26.25 26.571 26.25 61.25 0 34.68-26.285 60.93t-61.25 26.25ZM234-361l-60.5-59.5Q243-490 318-524.5T480-559q87 0 162.026 34.538Q717.053-489.923 786.5-421l-60 60q-61-60.5-122.532-86.25-61.533-25.75-124-25.75Q417.5-473 356-447.25T234-361ZM70.5-524.5l-60.5-60q91-93 211.947-150t258-57Q617-791.5 738-734.5t212 150l-60 60Q802.5-607 700.1-656.25t-220-49.25q-117.6 0-220.1 49.25T70.5-524.5Z"/></svg>
|
After Width: | Height: | Size: 605 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m803.5-74-392-391.5Q357-453 312.25-424T234-361l-60.5-59.5q37-37 75.75-64.75T343.5-535l-108-108q-47 23-88.5 54t-76.5 64.5L10-584q36-37 76.5-68.75T170-707.5L76-802l41-41 727.5 727.5-41 41.5ZM480-133.5q-35 0-61.25-26T392.5-221q0-34.5 26.25-61T480-308.5q35 0 61.25 26.5t26.25 61q0 35.5-26.25 61.5t-61.25 26Zm245.5-226Q693-391 666-410.5t-68.5-38l-110-110q93 2 164.25 38.75T786.5-420.5l-61 61Zm164.5-165q-87.5-83-190.25-132T480-705.5q-37 0-69.5 4.5t-54 12l-70-70q42.5-15 91.75-23.75T480-791.5q137 0 257.5 56.75T950-584l-60 59.5Z"/></svg>
|
After Width: | Height: | Size: 628 B |
@ -113,7 +113,7 @@ object NtfManager {
|
||||
|
||||
private fun prepareIconPath(icon: ImageBitmap?): String? = if (icon != null) {
|
||||
tmpDir.mkdir()
|
||||
val newFile = File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", "png"))
|
||||
val newFile = File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", "png", tmpDir))
|
||||
try {
|
||||
ImageIO.write(icon.toAwtImage(), "PNG", newFile.outputStream())
|
||||
newFile.absolutePath
|
||||
|
@ -2,11 +2,15 @@ package chat.simplex.common.platform
|
||||
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.views.call.RcvCallInvitation
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.views.helpers.withBGApi
|
||||
import java.util.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
actual val appPlatform = AppPlatform.DESKTOP
|
||||
|
||||
actual val deviceName = generalGetString(MR.strings.desktop_device)
|
||||
|
||||
@Suppress("ConstantLocale")
|
||||
val defaultLocale: Locale = Locale.getDefault()
|
||||
|
||||
|
@ -21,6 +21,8 @@ actual val agentDatabaseFileName: String = "simplex_v1_agent.db"
|
||||
|
||||
actual val databaseExportDir: File = tmpDir
|
||||
|
||||
actual val remoteHostsDir: File = File(dataDir.absolutePath + File.separator + "remote_hosts")
|
||||
|
||||
actual fun desktopOpenDatabaseDir() {
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
|
@ -50,22 +50,23 @@ actual fun ActiveCallView() {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) {
|
||||
Log.d(TAG, "has active call $call")
|
||||
val callRh = call.remoteHostId
|
||||
when (val r = apiMsg.resp) {
|
||||
is WCallResponse.Capabilities -> withBGApi {
|
||||
val callType = CallType(call.localMedia, r.capabilities)
|
||||
chatModel.controller.apiSendCallInvitation(call.contact, callType)
|
||||
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
|
||||
}
|
||||
is WCallResponse.Offer -> withBGApi {
|
||||
chatModel.controller.apiSendCallOffer(call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
|
||||
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
|
||||
}
|
||||
is WCallResponse.Answer -> withBGApi {
|
||||
chatModel.controller.apiSendCallAnswer(call.contact, r.answer, r.iceCandidates)
|
||||
chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
|
||||
}
|
||||
is WCallResponse.Ice -> withBGApi {
|
||||
chatModel.controller.apiSendCallExtraInfo(call.contact, r.iceCandidates)
|
||||
chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates)
|
||||
}
|
||||
is WCallResponse.Connection ->
|
||||
try {
|
||||
@ -73,7 +74,7 @@ actual fun ActiveCallView() {
|
||||
if (callStatus == WebRTCCallStatus.Connected) {
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectedAt = Clock.System.now())
|
||||
}
|
||||
withBGApi { chatModel.controller.apiCallStatus(call.contact, callStatus) }
|
||||
withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) }
|
||||
} catch (e: Error) {
|
||||
Log.d(TAG, "call status ${r.state.connectionState} not used")
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
package chat.simplex.common.views.chat
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
actual fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
ScanCodeLayout(verifyCode, close)
|
||||
}
|
@ -2,12 +2,10 @@ package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import chat.simplex.common.model.CIFile
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.ModalManager
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
actual fun SimpleAndAnimatedImageView(
|
||||
|
@ -34,35 +34,51 @@ actual fun ReactionIcon(text: String, fontSize: TextUnit) {
|
||||
@Composable
|
||||
actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = {
|
||||
when (cItem.content.msgContent) {
|
||||
is MsgContent.MCImage, is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") }
|
||||
else -> {}
|
||||
val saveIfExists = {
|
||||
when (cItem.content.msgContent) {
|
||||
is MsgContent.MCImage, is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") }
|
||||
else -> {}
|
||||
}
|
||||
showMenu.value = false
|
||||
}
|
||||
showMenu.value = false
|
||||
var fileSource = getLoadedFileSource(cItem.file)
|
||||
if (chatModel.connectedToRemote() && fileSource == null) {
|
||||
withBGApi {
|
||||
cItem.file?.loadRemoteFile(true)
|
||||
fileSource = getLoadedFileSource(cItem.file)
|
||||
saveIfExists()
|
||||
}
|
||||
} else saveIfExists()
|
||||
})
|
||||
}
|
||||
|
||||
actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) {
|
||||
val fileSource = getLoadedFileSource(cItem.file)
|
||||
actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) = withBGApi {
|
||||
var fileSource = getLoadedFileSource(cItem.file)
|
||||
if (chatModel.connectedToRemote() && fileSource == null) {
|
||||
cItem.file?.loadRemoteFile(true)
|
||||
fileSource = getLoadedFileSource(cItem.file)
|
||||
}
|
||||
|
||||
if (fileSource != null) {
|
||||
val filePath: String = if (fileSource.cryptoArgs != null) {
|
||||
val tmpFile = File(tmpDir, fileSource.filePath)
|
||||
tmpFile.deleteOnExit()
|
||||
try {
|
||||
decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, tmpFile.absolutePath)
|
||||
decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs ?: return@withBGApi, tmpFile.absolutePath)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString())
|
||||
return
|
||||
return@withBGApi
|
||||
}
|
||||
tmpFile.absolutePath
|
||||
} else {
|
||||
getAppFilePath(fileSource.filePath)
|
||||
}
|
||||
when {
|
||||
when {
|
||||
desktopPlatform.isWindows() -> clipboard.setText(AnnotatedString("\"${File(filePath).absolutePath}\""))
|
||||
else -> clipboard.setText(AnnotatedString(filePath))
|
||||
}
|
||||
} else {
|
||||
clipboard.setText(AnnotatedString(cItem.content.text))
|
||||
}
|
||||
}
|
||||
showToast(MR.strings.copied.localized())
|
||||
}.run {}
|
||||
|
@ -44,7 +44,7 @@ actual fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<An
|
||||
val chat = chatModel.getChat(call.contact.id)
|
||||
if (chat != null) {
|
||||
withApi {
|
||||
openChat(chat.chatInfo, chatModel)
|
||||
openChat(chat.remoteHostId, chat.chatInfo, chatModel)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user