mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 17:13:10 +00:00
ui: plan and connect spinners (#6048)
* ios: plan and connect spinners * fix cleanup * remove redundant check * revert diff * progress by timeout for join and connect buttons * accept in progress * connect in progress by timeout wip * fix observable * fix cleanup * kotlin wip * decrease delay * ios: spinner as overlay * remove duplicate stop progress, remove cleanupOnCancel * contact request progress * plan in progress wip * fix cleanup * search * fix ios * scanner * desktop paste * spinner in chat * ios: don't repeat scan while in progress * spinner in paste field * cancellable plan * cancel in more scenarios * fix cancellation * move progress to search * remove task var * pause scanner when processing qr code * kotlin * improvements * layout * layout * move spinner --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
1 parent
ca22cd4367
commit
a7f8866360
21 files changed
+743
-379
No files matched your search
@@ -314,6 +314,41 @@ class ChatItemDummyModel: ObservableObject {
|
||||
func sendUpdate() { objectWillChange.send() }
|
||||
}
|
||||
|
||||
class ConnectProgressManager: ObservableObject {
|
||||
@Published private var connectInProgress: String? = nil
|
||||
@Published private var connectProgressByTimeout: Bool = false
|
||||
private var onCancel: (() -> Void)?
|
||||
|
||||
static let shared = ConnectProgressManager()
|
||||
|
||||
func startConnectProgress(_ text: String, onCancel: (() -> Void)? = nil) {
|
||||
connectInProgress = text
|
||||
self.onCancel = onCancel
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
self.connectProgressByTimeout = self.connectInProgress != nil
|
||||
}
|
||||
}
|
||||
|
||||
func stopConnectProgress() {
|
||||
connectInProgress = nil
|
||||
onCancel = nil
|
||||
connectProgressByTimeout = false
|
||||
}
|
||||
|
||||
func cancelConnectProgress() {
|
||||
onCancel?()
|
||||
stopConnectProgress()
|
||||
}
|
||||
|
||||
var showConnectProgress: String? {
|
||||
connectProgressByTimeout ? connectInProgress : nil
|
||||
}
|
||||
|
||||
var isInProgress: Bool {
|
||||
connectInProgress != nil
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
@Published var setDeliveryReceipts = false
|
||||
|
||||
@@ -120,9 +120,10 @@ func chatSendCmd<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDe
|
||||
return try apiResult(res)
|
||||
}
|
||||
|
||||
func chatApiSendCmdWithRetry<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, retryNum: Int32 = 0) async -> APIResult<R>? {
|
||||
func chatApiSendCmdWithRetry<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, inProgress: BoxedValue<Bool>? = nil, retryNum: Int32 = 0) async -> APIResult<R>? {
|
||||
let r: APIResult<R> = await chatApiSendCmd(cmd, bgTask: bgTask, bgDelay: bgDelay, retryNum: retryNum)
|
||||
if case let .error(e) = r, let alert = retryableNetworkErrorAlert(e) {
|
||||
if inProgress == nil || inProgress?.boxedValue == true,
|
||||
case let .error(e) = r, let alert = retryableNetworkErrorAlert(e) {
|
||||
return await withCheckedContinuation { cont in
|
||||
showRetryAlert(
|
||||
alert,
|
||||
@@ -130,7 +131,7 @@ func chatApiSendCmdWithRetry<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool
|
||||
cont.resume(returning: nil)
|
||||
},
|
||||
onRetry: {
|
||||
let r1: APIResult<R>? = await chatApiSendCmdWithRetry(cmd, bgTask: bgTask, bgDelay: bgDelay, retryNum: retryNum + 1)
|
||||
let r1: APIResult<R>? = await chatApiSendCmdWithRetry(cmd, bgTask: bgTask, bgDelay: bgDelay, inProgress: inProgress, retryNum: retryNum + 1)
|
||||
cont.resume(returning: r1)
|
||||
}
|
||||
)
|
||||
@@ -980,12 +981,12 @@ func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> Pendi
|
||||
if let r { throw r.unexpected } else { return nil }
|
||||
}
|
||||
|
||||
func apiConnectPlan(connLink: String) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) {
|
||||
func apiConnectPlan(connLink: String, inProgress: BoxedValue<Bool>) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else {
|
||||
logger.error("apiConnectPlan: no current user")
|
||||
return (nil, nil)
|
||||
}
|
||||
let r: APIResult<ChatResponse1>? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink))
|
||||
let r: APIResult<ChatResponse1>? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink), inProgress: inProgress)
|
||||
if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) }
|
||||
let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil }
|
||||
return (nil, alert)
|
||||
|
||||
@@ -15,6 +15,7 @@ private let memberImageSize: CGFloat = 34
|
||||
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@StateObject private var connectProgressManager = ConnectProgressManager.shared
|
||||
@State var revealedItems: Set<Int64> = Set()
|
||||
@State var theme: AppTheme = buildTheme()
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@@ -114,6 +115,9 @@ struct ChatView: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
if let connectInProgressText = connectProgressManager.showConnectProgress {
|
||||
connectInProgressView(connectInProgressText)
|
||||
}
|
||||
if let connectingText {
|
||||
Text(connectingText)
|
||||
.font(.caption)
|
||||
@@ -262,6 +266,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
ConnectProgressManager.shared.cancelConnectProgress()
|
||||
scrollView.listState.onUpdateListener = onChatItemsUpdated
|
||||
selectedChatItems = nil
|
||||
revealedItems = Set()
|
||||
@@ -286,6 +291,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { cId in
|
||||
ConnectProgressManager.shared.cancelConnectProgress()
|
||||
showChatInfoSheet = false
|
||||
selectedChatItems = nil
|
||||
revealedItems = Set()
|
||||
@@ -341,6 +347,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
ConnectProgressManager.shared.cancelConnectProgress()
|
||||
VideoPlayerView.players.removeAll()
|
||||
stopAudioPlayer()
|
||||
if chatModel.chatId == cInfo.id && !presentationMode.wrappedValue.isPresented {
|
||||
@@ -386,6 +393,30 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func connectInProgressView(_ s: String) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
|
||||
HStack(spacing: 12) {
|
||||
ProgressView()
|
||||
Text(s)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
ConnectProgressManager.shared.cancelConnectProgress()
|
||||
} label: {
|
||||
Image(systemName: "multiply")
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
.padding(12)
|
||||
.frame(minHeight: 54)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(ToolbarMaterial.material(toolbarMaterial))
|
||||
}
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
@ViewBuilder private func primaryPrincipalToolbarContent() -> some View {
|
||||
let cInfo = chat.chatInfo
|
||||
|
||||
@@ -44,6 +44,7 @@ struct ComposeState {
|
||||
var contextItem: ComposeContextItem
|
||||
var voiceMessageRecordingState: VoiceMessageRecordingState
|
||||
var inProgress = false
|
||||
var progressByTimeout = false
|
||||
var useLinkPreviews: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
|
||||
var mentions: MentionedMembers = [:]
|
||||
|
||||
@@ -410,7 +411,14 @@ struct ComposeView: View {
|
||||
if chat.chatInfo.groupInfo?.nextConnectPrepared == true {
|
||||
if chat.chatInfo.groupInfo?.businessChat == nil {
|
||||
Button(action: connectPreparedGroup) {
|
||||
Label("Join group", systemImage: "person.2.fill")
|
||||
ZStack(alignment: .trailing) {
|
||||
Label("Join group", systemImage: "person.2.fill")
|
||||
.frame(maxWidth: .infinity)
|
||||
if composeState.progressByTimeout {
|
||||
ProgressView()
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 60)
|
||||
.disabled(composeState.inProgress)
|
||||
@@ -429,7 +437,14 @@ struct ComposeView: View {
|
||||
switch linkType {
|
||||
case .inv:
|
||||
Button(action: sendConnectPreparedContact) {
|
||||
Label("Connect", systemImage: "person.fill.badge.plus")
|
||||
ZStack(alignment: .trailing) {
|
||||
Label("Connect", systemImage: "person.fill.badge.plus")
|
||||
.frame(maxWidth: .infinity)
|
||||
if composeState.progressByTimeout {
|
||||
ProgressView()
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 60)
|
||||
.disabled(composeState.inProgress)
|
||||
@@ -474,6 +489,15 @@ struct ComposeView: View {
|
||||
clearState()
|
||||
}
|
||||
}
|
||||
.onChange(of: composeState.inProgress) { inProgress in
|
||||
if inProgress {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
composeState.progressByTimeout = composeState.inProgress
|
||||
}
|
||||
} else {
|
||||
composeState.progressByTimeout = false
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Attach", isPresented: $showChooseSource, titleVisibility: .visible) {
|
||||
Button("Take picture") {
|
||||
showTakePhoto = true
|
||||
|
||||
@@ -14,6 +14,7 @@ struct ContextContactRequestActionsView: View {
|
||||
var contactRequestId: Int64
|
||||
@UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
||||
@State private var inProgress = false
|
||||
@State private var progressByTimeout = false
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
@@ -21,7 +22,7 @@ struct ContextContactRequestActionsView: View {
|
||||
Label("Reject", systemImage: "multiply")
|
||||
}
|
||||
.frame(maxWidth: .infinity, minHeight: 60)
|
||||
|
||||
|
||||
Button {
|
||||
if ChatModel.shared.addressShortLinkDataSet {
|
||||
acceptRequest()
|
||||
@@ -36,6 +37,22 @@ struct ContextContactRequestActionsView: View {
|
||||
.disabled(inProgress)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(ToolbarMaterial.material(toolbarMaterial))
|
||||
.opacity(progressByTimeout ? 0.4 : 1)
|
||||
.overlay {
|
||||
if progressByTimeout {
|
||||
ProgressView()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
.onChange(of: inProgress) { inPrgrs in
|
||||
if inPrgrs {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
progressByTimeout = inProgress
|
||||
}
|
||||
} else {
|
||||
progressByTimeout = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showRejectRequestAlert() {
|
||||
|
||||
@@ -44,7 +44,6 @@ struct SendMessageView: View {
|
||||
@State private var showCustomDisappearingMessageDialogue = false
|
||||
@State private var showCustomTimePicker = false
|
||||
@State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get()
|
||||
@State private var progressByTimeout = false
|
||||
@UserDefault(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
|
||||
|
||||
var body: some View {
|
||||
@@ -76,12 +75,12 @@ struct SendMessageView: View {
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .topTrailing, content: {
|
||||
if !progressByTimeout && teHeight > 100 && !composeState.inProgress {
|
||||
if !composeState.progressByTimeout && teHeight > 100 && !composeState.inProgress {
|
||||
deleteTextButton()
|
||||
}
|
||||
})
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
if progressByTimeout {
|
||||
if composeState.progressByTimeout {
|
||||
ProgressView()
|
||||
.scaleEffect(1.4)
|
||||
.frame(width: 31, height: 31, alignment: .center)
|
||||
@@ -97,15 +96,6 @@ struct SendMessageView: View {
|
||||
.clipShape(composeShape)
|
||||
.overlay(composeShape.strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7))
|
||||
.onChange(of: composeState.message, perform: { text in updateFont(text) })
|
||||
.onChange(of: composeState.inProgress) { inProgress in
|
||||
if inProgress {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
progressByTimeout = composeState.inProgress
|
||||
}
|
||||
} else {
|
||||
progressByTimeout = false
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ struct UserPickerSheetView: View {
|
||||
|
||||
struct ChatListView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@StateObject private var connectProgressManager = ConnectProgressManager.shared
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var activeUserPickerSheet: UserPickerSheet?
|
||||
@State private var searchMode = false
|
||||
@@ -572,6 +573,7 @@ struct ChatListSearchBar: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@EnvironmentObject var chatTagsModel: ChatTagsModel
|
||||
@StateObject private var connectProgressManager = ConnectProgressManager.shared
|
||||
@Binding var searchMode: Bool
|
||||
@FocusState.Binding var searchFocussed: Bool
|
||||
@Binding var searchText: String
|
||||
@@ -591,6 +593,9 @@ struct ChatListSearchBar: View {
|
||||
.disabled(searchShowingSimplexLink)
|
||||
.focused($searchFocussed)
|
||||
.frame(maxWidth: .infinity)
|
||||
if connectProgressManager.showConnectProgress != nil {
|
||||
ProgressView()
|
||||
}
|
||||
if !searchText.isEmpty {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.onTapGesture {
|
||||
@@ -634,6 +639,8 @@ struct ChatListSearchBar: View {
|
||||
} else {
|
||||
if t != "" { // if some other text is pasted, enter search mode
|
||||
searchFocussed = true
|
||||
} else {
|
||||
ConnectProgressManager.shared.cancelConnectProgress()
|
||||
}
|
||||
searchShowingSimplexLink = false
|
||||
searchChatFilteredBySimplexLink = nil
|
||||
|
||||
@@ -16,7 +16,8 @@ struct NewChatMenuButton: View {
|
||||
@State private var alert: SomeAlert? = nil
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
Button {
|
||||
ConnectProgressManager.shared.cancelConnectProgress()
|
||||
showNewChatSheet = true
|
||||
} label: {
|
||||
Image(systemName: "square.and.pencil")
|
||||
@@ -65,6 +66,8 @@ struct NewChatSheet: View {
|
||||
.alert(item: $alert) { a in
|
||||
return a.alert
|
||||
}
|
||||
}.onDisappear {
|
||||
ConnectProgressManager.shared.cancelConnectProgress()
|
||||
}
|
||||
if #available(iOS 16.0, *), oneHandUI {
|
||||
let sheetHeight: CGFloat = showArchive ? 575 : 500
|
||||
@@ -315,6 +318,7 @@ struct ContactsList: View {
|
||||
|
||||
struct ContactsListSearchBar: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@StateObject private var connectProgressManager = ConnectProgressManager.shared
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var searchMode: Bool
|
||||
@FocusState.Binding var searchFocussed: Bool
|
||||
@@ -338,6 +342,9 @@ struct ContactsListSearchBar: View {
|
||||
.disabled(searchShowingSimplexLink)
|
||||
.focused($searchFocussed)
|
||||
.frame(maxWidth: .infinity)
|
||||
if connectProgressManager.showConnectProgress != nil {
|
||||
ProgressView()
|
||||
}
|
||||
if !searchText.isEmpty {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.resizable()
|
||||
@@ -384,6 +391,8 @@ struct ContactsListSearchBar: View {
|
||||
} else {
|
||||
if t != "" { // if some other text is pasted, enter search mode
|
||||
searchFocussed = true
|
||||
} else {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
}
|
||||
searchShowingSimplexLink = false
|
||||
searchChatFilteredBySimplexLink = nil
|
||||
|
||||
@@ -585,11 +585,13 @@ private struct ActiveProfilePicker: View {
|
||||
}
|
||||
|
||||
private struct ConnectView: View {
|
||||
@StateObject private var connectProgressManager = ConnectProgressManager.shared
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var showQRCodeScanner: Bool
|
||||
@Binding var pastedLink: String
|
||||
@Binding var alert: NewChatViewAlert?
|
||||
@State var scannerPaused: Bool = false
|
||||
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
|
||||
|
||||
var body: some View {
|
||||
@@ -598,36 +600,49 @@ private struct ConnectView: View {
|
||||
pasteLinkView()
|
||||
}
|
||||
Section(header: Text("Or scan QR code").foregroundColor(theme.colors.secondary)) {
|
||||
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processQRCode)
|
||||
ScannerInView(showQRCodeScanner: $showQRCodeScanner, scannerPaused: $scannerPaused, processQRCode: processQRCode)
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func pasteLinkView() -> some View {
|
||||
if pastedLink == "" {
|
||||
Button {
|
||||
if let str = UIPasteboard.general.string {
|
||||
if let link = strHasSingleSimplexLink(str.trimmingCharacters(in: .whitespaces)) {
|
||||
pastedLink = link.text
|
||||
// It would be good to hide it, but right now it is not clear how to release camera in CodeScanner
|
||||
// https://github.com/twostraws/CodeScanner/issues/121
|
||||
// No known tricks worked (changing view ID, wrapping it in another view, etc.)
|
||||
// showQRCodeScanner = false
|
||||
connect(pastedLink)
|
||||
} else {
|
||||
alert = .newChatSomeAlert(alert: SomeAlert(
|
||||
alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."),
|
||||
id: "pasteLinkView: code is not a SimpleX link"
|
||||
))
|
||||
ZStack(alignment: .trailing) {
|
||||
Button {
|
||||
if let str = UIPasteboard.general.string {
|
||||
if let link = strHasSingleSimplexLink(str.trimmingCharacters(in: .whitespaces)) {
|
||||
pastedLink = link.text
|
||||
// It would be good to hide it, but right now it is not clear how to release camera in CodeScanner
|
||||
// https://github.com/twostraws/CodeScanner/issues/121
|
||||
// No known tricks worked (changing view ID, wrapping it in another view, etc.)
|
||||
// showQRCodeScanner = false
|
||||
connect(pastedLink)
|
||||
} else {
|
||||
alert = .newChatSomeAlert(alert: SomeAlert(
|
||||
alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."),
|
||||
id: "pasteLinkView: code is not a SimpleX link"
|
||||
))
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Tap to paste link")
|
||||
}
|
||||
.disabled(!pasteboardHasStrings)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
if connectProgressManager.showConnectProgress != nil {
|
||||
ProgressView()
|
||||
}
|
||||
} label: {
|
||||
Text("Tap to paste link")
|
||||
}
|
||||
.disabled(!pasteboardHasStrings)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
} else {
|
||||
linkTextView(pastedLink)
|
||||
HStack {
|
||||
linkTextView(pastedLink)
|
||||
if connectProgressManager.showConnectProgress != nil {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,16 +668,22 @@ private struct ConnectView: View {
|
||||
}
|
||||
|
||||
private func connect(_ link: String) {
|
||||
scannerPaused = true
|
||||
planAndConnect(
|
||||
link,
|
||||
theme: theme,
|
||||
dismiss: true
|
||||
dismiss: true,
|
||||
cleanup: {
|
||||
pastedLink = ""
|
||||
scannerPaused = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ScannerInView: View {
|
||||
@Binding var showQRCodeScanner: Bool
|
||||
var scannerPaused: Binding<Bool>? = nil
|
||||
let processQRCode: (_ resp: Result<ScanResult, ScanError>) -> Void
|
||||
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
|
||||
var scanMode: ScanMode = .continuous
|
||||
@@ -670,7 +691,7 @@ struct ScannerInView: View {
|
||||
var body: some View {
|
||||
Group {
|
||||
if showQRCodeScanner, case .authorized = cameraAuthorizationStatus {
|
||||
CodeScannerView(codeTypes: [.qr], scanMode: scanMode, completion: processQRCode)
|
||||
CodeScannerView(codeTypes: [.qr], scanMode: scanMode, isPaused: scannerPaused?.wrappedValue ?? false, completion: processQRCode)
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.cornerRadius(12)
|
||||
.listRowBackground(Color.clear)
|
||||
@@ -1148,231 +1169,249 @@ func planAndConnect(
|
||||
filterKnownContact: ((Contact) -> Void)? = nil,
|
||||
filterKnownGroup: ((GroupInfo) -> Void)? = nil
|
||||
) {
|
||||
Task {
|
||||
let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink)
|
||||
if let (connectionLink, connectionPlan) = result {
|
||||
switch connectionPlan {
|
||||
case let .invitationLink(ilp):
|
||||
switch ilp {
|
||||
case let .ok(contactSLinkData_):
|
||||
if let contactSLinkData = contactSLinkData_ {
|
||||
logger.debug("planAndConnect, .invitationLink, .ok, short link data present")
|
||||
await MainActor.run {
|
||||
showPrepareContactAlert(
|
||||
connectionLink: connectionLink,
|
||||
contactShortLinkData: contactSLinkData,
|
||||
theme: theme,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
ConnectProgressManager.shared.cancelConnectProgress()
|
||||
let inProgress = BoxedValue(true)
|
||||
connectTask(inProgress)
|
||||
ConnectProgressManager.shared.startConnectProgress(NSLocalizedString("Loading profile…", comment: "in progress text")) {
|
||||
inProgress.boxedValue = false
|
||||
cleanup?()
|
||||
}
|
||||
|
||||
func connectTask(_ inProgress: BoxedValue<Bool>) {
|
||||
Task {
|
||||
let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink, inProgress: inProgress)
|
||||
await MainActor.run {
|
||||
ConnectProgressManager.shared.stopConnectProgress()
|
||||
}
|
||||
if !inProgress.boxedValue { return }
|
||||
if let (connectionLink, connectionPlan) = result {
|
||||
switch connectionPlan {
|
||||
case let .invitationLink(ilp):
|
||||
switch ilp {
|
||||
case let .ok(contactSLinkData_):
|
||||
if let contactSLinkData = contactSLinkData_ {
|
||||
logger.debug("planAndConnect, .invitationLink, .ok, short link data present")
|
||||
await MainActor.run {
|
||||
showPrepareContactAlert(
|
||||
connectionLink: connectionLink,
|
||||
contactShortLinkData: contactSLinkData,
|
||||
theme: theme,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logger.debug("planAndConnect, .invitationLink, .ok, no short link data")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("Connect via one-time link", comment: "new chat sheet title"),
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug("planAndConnect, .invitationLink, .ok, no short link data")
|
||||
case .ownLink:
|
||||
logger.debug("planAndConnect, .invitationLink, .ownLink")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("Connect via one-time link", comment: "new chat sheet title"),
|
||||
title: NSLocalizedString("Connect to yourself?\nThis is your own one-time link!", comment: "new chat sheet title"),
|
||||
actionStyle: .destructive,
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
}
|
||||
case .ownLink:
|
||||
logger.debug("planAndConnect, .invitationLink, .ownLink")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("Connect to yourself?\nThis is your own one-time link!", comment: "new chat sheet title"),
|
||||
actionStyle: .destructive,
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
case let .connecting(contact_):
|
||||
logger.debug("planAndConnect, .invitationLink, .connecting")
|
||||
await MainActor.run {
|
||||
if let contact = contact_ {
|
||||
case let .connecting(contact_):
|
||||
logger.debug("planAndConnect, .invitationLink, .connecting")
|
||||
await MainActor.run {
|
||||
if let contact = contact_ {
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss)
|
||||
}
|
||||
} else {
|
||||
showInvitationLinkConnectingAlert(cleanup: cleanup)
|
||||
}
|
||||
}
|
||||
case let .known(contact):
|
||||
logger.debug("planAndConnect, .invitationLink, .known")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss)
|
||||
}
|
||||
} else {
|
||||
showInvitationLinkConnectingAlert(cleanup: cleanup)
|
||||
}
|
||||
}
|
||||
case let .known(contact):
|
||||
logger.debug("planAndConnect, .invitationLink, .known")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
case let .contactAddress(cap):
|
||||
switch cap {
|
||||
case let .ok(contactSLinkData_):
|
||||
if let contactSLinkData = contactSLinkData_ {
|
||||
logger.debug("planAndConnect, .contactAddress, .ok, short link data present")
|
||||
await MainActor.run {
|
||||
showPrepareContactAlert(
|
||||
connectionLink: connectionLink,
|
||||
contactShortLinkData: contactSLinkData,
|
||||
theme: theme,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
} else {
|
||||
showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss)
|
||||
logger.debug("planAndConnect, .contactAddress, .ok, no short link data")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("Connect via contact address", comment: "new chat sheet title"),
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .contactAddress(cap):
|
||||
switch cap {
|
||||
case let .ok(contactSLinkData_):
|
||||
if let contactSLinkData = contactSLinkData_ {
|
||||
logger.debug("planAndConnect, .contactAddress, .ok, short link data present")
|
||||
await MainActor.run {
|
||||
showPrepareContactAlert(
|
||||
connectionLink: connectionLink,
|
||||
contactShortLinkData: contactSLinkData,
|
||||
theme: theme,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logger.debug("planAndConnect, .contactAddress, .ok, no short link data")
|
||||
case .ownLink:
|
||||
logger.debug("planAndConnect, .contactAddress, .ownLink")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("Connect via contact address", comment: "new chat sheet title"),
|
||||
title: NSLocalizedString("Connect to yourself?\nThis is your own SimpleX address!", comment: "new chat sheet title"),
|
||||
actionStyle: .destructive,
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
}
|
||||
case .ownLink:
|
||||
logger.debug("planAndConnect, .contactAddress, .ownLink")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("Connect to yourself?\nThis is your own SimpleX address!", comment: "new chat sheet title"),
|
||||
actionStyle: .destructive,
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
case .connectingConfirmReconnect:
|
||||
logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("You have already requested connection!\nRepeat connection request?", comment: "new chat sheet title"),
|
||||
actionStyle: .destructive,
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
case let .connectingProhibit(contact):
|
||||
logger.debug("planAndConnect, .contactAddress, .connectingProhibit")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss)
|
||||
}
|
||||
}
|
||||
case let .known(contact):
|
||||
logger.debug("planAndConnect, .contactAddress, .known")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss)
|
||||
}
|
||||
}
|
||||
case let .contactViaAddress(contact):
|
||||
logger.debug("planAndConnect, .contactAddress, .contactViaAddress")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileConnectContactViaAddressSheet(
|
||||
contact: contact,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
}
|
||||
case let .groupLink(glp):
|
||||
switch glp {
|
||||
case let .ok(groupSLinkData_):
|
||||
if let groupSLinkData = groupSLinkData_ {
|
||||
logger.debug("planAndConnect, .groupLink, .ok, short link data present")
|
||||
await MainActor.run {
|
||||
showPrepareGroupAlert(
|
||||
connectionLink: connectionLink,
|
||||
groupShortLinkData: groupSLinkData,
|
||||
theme: theme,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logger.debug("planAndConnect, .groupLink, .ok, no short link data")
|
||||
case .connectingConfirmReconnect:
|
||||
logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("Join group", comment: "new chat sheet title"),
|
||||
title: NSLocalizedString("You have already requested connection!\nRepeat connection request?", comment: "new chat sheet title"),
|
||||
actionStyle: .destructive,
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
}
|
||||
case let .ownLink(groupInfo):
|
||||
logger.debug("planAndConnect, .groupLink, .ownLink")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownGroup {
|
||||
f(groupInfo)
|
||||
case let .connectingProhibit(contact):
|
||||
logger.debug("planAndConnect, .contactAddress, .connectingProhibit")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss)
|
||||
}
|
||||
}
|
||||
case let .known(contact):
|
||||
logger.debug("planAndConnect, .contactAddress, .known")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownContact {
|
||||
f(contact)
|
||||
} else {
|
||||
showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss)
|
||||
}
|
||||
}
|
||||
case let .contactViaAddress(contact):
|
||||
logger.debug("planAndConnect, .contactAddress, .contactViaAddress")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileConnectContactViaAddressSheet(
|
||||
contact: contact,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
showOwnGroupLinkConfirmConnectSheet(
|
||||
groupInfo: groupInfo,
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
case .connectingConfirmReconnect:
|
||||
logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("You are already joining the group!\nRepeat join request?", comment: "new chat sheet title"),
|
||||
actionStyle: .destructive,
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
case let .connectingProhibit(groupInfo_):
|
||||
logger.debug("planAndConnect, .groupLink, .connectingProhibit")
|
||||
await MainActor.run {
|
||||
showGroupLinkConnectingAlert(groupInfo: groupInfo_, cleanup: cleanup)
|
||||
}
|
||||
case let .known(groupInfo):
|
||||
logger.debug("planAndConnect, .groupLink, .known")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownGroup {
|
||||
f(groupInfo)
|
||||
case let .groupLink(glp):
|
||||
switch glp {
|
||||
case let .ok(groupSLinkData_):
|
||||
if let groupSLinkData = groupSLinkData_ {
|
||||
logger.debug("planAndConnect, .groupLink, .ok, short link data present")
|
||||
await MainActor.run {
|
||||
showPrepareGroupAlert(
|
||||
connectionLink: connectionLink,
|
||||
groupShortLinkData: groupSLinkData,
|
||||
theme: theme,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
} else {
|
||||
showOpenKnownGroupAlert(groupInfo, theme: theme, dismiss: dismiss)
|
||||
logger.debug("planAndConnect, .groupLink, .ok, no short link data")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("Join group", comment: "new chat sheet title"),
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
}
|
||||
case let .ownLink(groupInfo):
|
||||
logger.debug("planAndConnect, .groupLink, .ownLink")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownGroup {
|
||||
f(groupInfo)
|
||||
}
|
||||
showOwnGroupLinkConfirmConnectSheet(
|
||||
groupInfo: groupInfo,
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
case .connectingConfirmReconnect:
|
||||
logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect")
|
||||
await MainActor.run {
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("You are already joining the group!\nRepeat join request?", comment: "new chat sheet title"),
|
||||
actionStyle: .destructive,
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: connectionPlan,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
case let .connectingProhibit(groupInfo_):
|
||||
logger.debug("planAndConnect, .groupLink, .connectingProhibit")
|
||||
await MainActor.run {
|
||||
showGroupLinkConnectingAlert(groupInfo: groupInfo_, cleanup: cleanup)
|
||||
}
|
||||
case let .known(groupInfo):
|
||||
logger.debug("planAndConnect, .groupLink, .known")
|
||||
await MainActor.run {
|
||||
if let f = filterKnownGroup {
|
||||
f(groupInfo)
|
||||
} else {
|
||||
showOpenKnownGroupAlert(groupInfo, theme: theme, dismiss: dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .error(chatError):
|
||||
logger.debug("planAndConnect, .error \(chatErrorString(chatError))")
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("Connect via link", comment: "new chat sheet title"),
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: nil,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
case let .error(chatError):
|
||||
logger.debug("planAndConnect, .error \(chatErrorString(chatError))")
|
||||
showAskCurrentOrIncognitoProfileSheet(
|
||||
title: NSLocalizedString("Connect via link", comment: "new chat sheet title"),
|
||||
connectionLink: connectionLink,
|
||||
connectionPlan: nil,
|
||||
dismiss: dismiss,
|
||||
cleanup: cleanup
|
||||
)
|
||||
}
|
||||
} else if let alert {
|
||||
await MainActor.run {
|
||||
dismissAllSheets(animated: true) {
|
||||
AlertManager.shared.showAlert(alert)
|
||||
cleanup?()
|
||||
} else {
|
||||
await MainActor.run {
|
||||
if let alert {
|
||||
dismissAllSheets(animated: true) {
|
||||
AlertManager.shared.showAlert(alert)
|
||||
cleanup?()
|
||||
}
|
||||
} else {
|
||||
cleanup?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
@@ -43,6 +43,39 @@ import kotlin.collections.ArrayList
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.*
|
||||
|
||||
object ConnectProgressManager {
|
||||
private val connectInProgress = mutableStateOf<String?>(null)
|
||||
private val connectProgressByTimeout = mutableStateOf(false)
|
||||
private var onCancel: (() -> Unit)? = null
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
|
||||
fun startConnectProgress(text: String, onCancel: (() -> Unit)? = null) {
|
||||
connectInProgress.value = text
|
||||
this.onCancel = onCancel
|
||||
coroutineScope.launch {
|
||||
delay(1000)
|
||||
connectProgressByTimeout.value = connectInProgress.value != null
|
||||
}
|
||||
}
|
||||
|
||||
fun stopConnectProgress() {
|
||||
connectInProgress.value = null
|
||||
onCancel = null
|
||||
connectProgressByTimeout.value = false
|
||||
}
|
||||
|
||||
fun cancelConnectProgress() {
|
||||
onCancel?.invoke()
|
||||
stopConnectProgress()
|
||||
}
|
||||
|
||||
val showConnectProgress: String? get() =
|
||||
if (connectProgressByTimeout.value) connectInProgress.value else null
|
||||
}
|
||||
|
||||
val connectProgressManager = ConnectProgressManager
|
||||
|
||||
/*
|
||||
* Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it
|
||||
* */
|
||||
|
||||
+32
-31
@@ -673,32 +673,35 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendCmdWithRetry(rhId: Long?, cmd: CC, retryNum: Int = 0): API? {
|
||||
private suspend fun sendCmdWithRetry(rhId: Long?, cmd: CC, inProgress: MutableState<Boolean>? = null, retryNum: Int = 0): API? {
|
||||
val r = sendCmd(rhId, cmd, retryNum = retryNum)
|
||||
val alert = if (r is API.Error) retryableNetworkErrorAlert(r.err) else null
|
||||
if (alert == null) return r
|
||||
else return suspendCancellableCoroutine { cont ->
|
||||
showRetryAlert(
|
||||
alert,
|
||||
onCancel = {
|
||||
cont.resumeWith(Result.success(null))
|
||||
},
|
||||
onRetry = {
|
||||
withLongRunningApi {
|
||||
cont.resumeWith(
|
||||
runCatching {
|
||||
coroutineScope {
|
||||
sendCmdWithRetry(rhId, cmd, retryNum = retryNum + 1)
|
||||
if ((inProgress == null || inProgress.value) && alert != null) {
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
showRetryAlert(
|
||||
alert,
|
||||
onCancel = {
|
||||
cont.resumeWith(Result.success(null))
|
||||
},
|
||||
onRetry = {
|
||||
withLongRunningApi {
|
||||
cont.resumeWith(
|
||||
runCatching {
|
||||
coroutineScope {
|
||||
sendCmdWithRetry(rhId, cmd, inProgress = inProgress, retryNum = retryNum + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
cont.invokeOnCancellation {
|
||||
cont.resumeWith(Result.success(null))
|
||||
cont.invokeOnCancellation {
|
||||
cont.resumeWith(Result.success(null))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1408,11 +1411,11 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiConnectPlan(rh: Long?, connLink: String): Pair<CreatedConnLink, ConnectionPlan>? {
|
||||
suspend fun apiConnectPlan(rh: Long?, connLink: String, inProgress: MutableState<Boolean>): Pair<CreatedConnLink, ConnectionPlan>? {
|
||||
val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null }
|
||||
val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink))
|
||||
val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink), inProgress = inProgress)
|
||||
if (r is API.Result && r.res is CR.CRConnectionPlan) return r.res.connLink to r.res.connectionPlan
|
||||
if (r != null) apiConnectResponseAlert(r)
|
||||
if (inProgress.value && r != null) apiConnectResponseAlert(r)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1517,10 +1520,9 @@ object ChatController {
|
||||
suspend fun apiConnectPreparedContact(rh: Long?, contactId: Long, incognito: Boolean, msg: MsgContent?): Contact? {
|
||||
val r = sendCmdWithRetry(rh, CC.APIConnectPreparedContact(contactId, incognito, msg))
|
||||
if (r is API.Result && r.res is CR.StartedConnectionToContact) return r.res.contact
|
||||
if (r == null) return null
|
||||
Log.e(TAG, "apiConnectPreparedContact bad response: ${r.responseType} ${r.details}")
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiConnectPreparedContact", generalGetString(MR.strings.connection_error), r)
|
||||
if (r != null) {
|
||||
Log.e(TAG, "apiConnectPreparedContact bad response: ${r.responseType} ${r.details}")
|
||||
apiConnectResponseAlert(r)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -1528,10 +1530,9 @@ object ChatController {
|
||||
suspend fun apiConnectPreparedGroup(rh: Long?, groupId: Long, incognito: Boolean, msg: MsgContent?): GroupInfo? {
|
||||
val r = sendCmdWithRetry(rh, CC.APIConnectPreparedGroup(groupId, incognito, msg))
|
||||
if (r is API.Result && r.res is CR.StartedConnectionToGroup) return r.res.groupInfo
|
||||
if (r == null) return null
|
||||
Log.e(TAG, "apiConnectPreparedGroup bad response: ${r.responseType} ${r.details}")
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiConnectPreparedGroup", generalGetString(MR.strings.connection_error), r)
|
||||
if (r != null) {
|
||||
Log.e(TAG, "apiConnectPreparedGroup bad response: ${r.responseType} ${r.details}")
|
||||
apiConnectResponseAlert(r)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
+48
@@ -53,6 +53,40 @@ import kotlin.math.*
|
||||
@Stable
|
||||
data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val date: Instant?)
|
||||
|
||||
@Composable
|
||||
fun ConnectInProgressView(s: String) {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
Divider()
|
||||
Row(
|
||||
Modifier
|
||||
.height(60.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.fillMaxWidth()
|
||||
.weight(1F),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ComposeProgressIndicator()
|
||||
Text(s)
|
||||
}
|
||||
|
||||
IconButton(onClick = { connectProgressManager.cancelConnectProgress() }) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close),
|
||||
contentDescription = stringResource(MR.strings.cancel_verb),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
// staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat
|
||||
// to chat list smooth. Otherwise, chat view will become blank right before the transition starts
|
||||
@@ -102,6 +136,13 @@ fun ChatView(
|
||||
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val selectedChatItems = rememberSaveable { mutableStateOf(null as Set<Long>?) }
|
||||
if (appPlatform.isAndroid) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
// snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value.
|
||||
// With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view
|
||||
@@ -110,6 +151,9 @@ fun ChatView(
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.collect { chatId ->
|
||||
if (appPlatform.isAndroid) {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
}
|
||||
if (chatsCtx.secondaryContextFilter == null) {
|
||||
markUnreadChatAsRead(chatId)
|
||||
}
|
||||
@@ -177,6 +221,10 @@ fun ChatView(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val connectInProgressText = connectProgressManager.showConnectProgress
|
||||
if (appPlatform.isAndroid && connectInProgressText != null) {
|
||||
ConnectInProgressView(connectInProgressText)
|
||||
}
|
||||
val connectingText = connectingText(chatInfo)
|
||||
if (connectingText != null) {
|
||||
Text(
|
||||
|
||||
+79
-51
@@ -6,9 +6,11 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -19,68 +21,94 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun ComposeContextContactRequestActionsView(
|
||||
rhId: Long?,
|
||||
contactRequestId: Long
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.height(60.dp)
|
||||
.background(MaterialTheme.colors.surface)
|
||||
val inProgress = rememberSaveable { mutableStateOf(false) }
|
||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(inProgress.value) {
|
||||
progressByTimeout = if (inProgress.value) {
|
||||
delay(1000)
|
||||
inProgress.value
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.height(60.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Divider()
|
||||
|
||||
Row(
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
.background(MaterialTheme.colors.surface)
|
||||
.alpha(if (progressByTimeout) 0.6f else 1f)
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.weight(1F)
|
||||
.clickable {
|
||||
showRejectRequestAlert(rhId, contactRequestId)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close),
|
||||
contentDescription = null,
|
||||
tint = Color.Red,
|
||||
)
|
||||
TextIconSpaced(false)
|
||||
Text(stringResource(MR.strings.reject_contact_button), color = Color.Red)
|
||||
}
|
||||
Divider()
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.weight(1F)
|
||||
.clickable {
|
||||
if (chatModel.addressShortLinkDataSet()) {
|
||||
acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel)
|
||||
} else {
|
||||
showAcceptRequestAlert(rhId, contactRequestId)
|
||||
}
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_check),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
)
|
||||
TextIconSpaced(false)
|
||||
Text(stringResource(MR.strings.accept_contact_button), color = MaterialTheme.colors.primary)
|
||||
var rejectButtonModifier = Modifier.fillMaxWidth().fillMaxHeight().weight(1F)
|
||||
rejectButtonModifier =
|
||||
if (inProgress.value) rejectButtonModifier
|
||||
else rejectButtonModifier.clickable { showRejectRequestAlert(rhId, contactRequestId) }
|
||||
Row(
|
||||
rejectButtonModifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_close),
|
||||
contentDescription = null,
|
||||
tint = if (inProgress.value) MaterialTheme.colors.secondary else Color.Red,
|
||||
)
|
||||
TextIconSpaced(false)
|
||||
Text(
|
||||
stringResource(MR.strings.reject_contact_button),
|
||||
color = if (inProgress.value) MaterialTheme.colors.secondary else Color.Red
|
||||
)
|
||||
}
|
||||
var acceptButtonModifier = Modifier.fillMaxWidth().fillMaxHeight().weight(1F)
|
||||
acceptButtonModifier =
|
||||
if (inProgress.value) acceptButtonModifier
|
||||
else
|
||||
acceptButtonModifier.clickable {
|
||||
if (chatModel.addressShortLinkDataSet()) {
|
||||
acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel = chatModel, close = null, inProgress = inProgress)
|
||||
} else {
|
||||
showAcceptRequestAlert(rhId, contactRequestId, inProgress = inProgress)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
acceptButtonModifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_check),
|
||||
contentDescription = null,
|
||||
tint = if (inProgress.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
|
||||
)
|
||||
TextIconSpaced(false)
|
||||
Text(
|
||||
stringResource(MR.strings.accept_contact_button),
|
||||
color = if (inProgress.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (progressByTimeout) {
|
||||
ComposeProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +126,7 @@ fun showRejectRequestAlert(rhId: Long?, contactRequestId: Long) {
|
||||
)
|
||||
}
|
||||
|
||||
fun showAcceptRequestAlert(rhId: Long?, contactRequestId: Long) {
|
||||
fun showAcceptRequestAlert(rhId: Long?, contactRequestId: Long, inProgress: MutableState<Boolean>) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.accept_contact_request),
|
||||
buttons = {
|
||||
@@ -106,14 +134,14 @@ fun showAcceptRequestAlert(rhId: Long?, contactRequestId: Long) {
|
||||
// Accept
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel = chatModel, close = null, inProgress = inProgress)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
// Accept incognito
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
acceptContactRequest(rhId, incognito = true, contactRequestId, isCurrentUser = true, chatModel)
|
||||
acceptContactRequest(rhId, incognito = true, contactRequestId, isCurrentUser = true, chatModel = chatModel, close = null, inProgress = inProgress)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
|
||||
+25
-4
@@ -97,6 +97,7 @@ data class ComposeState(
|
||||
val preview: ComposePreview = ComposePreview.NoPreview,
|
||||
val contextItem: ComposeContextItem = ComposeContextItem.NoContextItem,
|
||||
val inProgress: Boolean = false,
|
||||
val progressByTimeout: Boolean = false,
|
||||
val useLinkPreviews: Boolean,
|
||||
val mentions: MentionedMembers = emptyMap()
|
||||
) {
|
||||
@@ -427,7 +428,7 @@ fun ComposeView(
|
||||
|
||||
fun clearState(live: Boolean = false) {
|
||||
if (live) {
|
||||
composeState.value = composeState.value.copy(inProgress = false)
|
||||
composeState.value = composeState.value.copy(inProgress = false, progressByTimeout = false)
|
||||
} else {
|
||||
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
resetLinkPreview()
|
||||
@@ -1245,6 +1246,7 @@ fun ComposeView(
|
||||
SimpleButtonIconEnded(
|
||||
text = stringResource(MR.strings.compose_view_connect),
|
||||
icon = painterResource(icon),
|
||||
color = if (composeState.value.inProgress) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
|
||||
disabled = composeState.value.inProgress,
|
||||
click = { withApi { sendRequest() } }
|
||||
)
|
||||
@@ -1265,19 +1267,28 @@ fun ComposeView(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painterResource(icon),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
tint = if (composeState.value.inProgress) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.primary
|
||||
color = if (composeState.value.inProgress) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
if (composeState.value.progressByTimeout) {
|
||||
Box(
|
||||
Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING_HALF),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
ComposeProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1328,6 +1339,16 @@ fun ComposeView(
|
||||
chatModel.sharedContent.value = null
|
||||
}
|
||||
|
||||
LaunchedEffect(composeState.value.inProgress) {
|
||||
val newProgressByTimeout = if (composeState.value.inProgress) {
|
||||
delay(1000)
|
||||
composeState.value.inProgress
|
||||
} else {
|
||||
false
|
||||
}
|
||||
composeState.value = composeState.value.copy(progressByTimeout = newProgressByTimeout)
|
||||
}
|
||||
|
||||
Column {
|
||||
val currentUser = chatModel.currentUser.value
|
||||
if (chat.chatInfo.nextConnectPrepared && currentUser != null) {
|
||||
|
||||
+3
-12
@@ -65,15 +65,6 @@ fun SendMsgView(
|
||||
val padding = if (appPlatform.isAndroid) PaddingValues(vertical = 8.dp) else PaddingValues(top = 3.dp, bottom = 4.dp)
|
||||
Box(Modifier.padding(padding)) {
|
||||
val cs = composeState.value
|
||||
var progressByTimeout by rememberSaveable { mutableStateOf(false) }
|
||||
LaunchedEffect(composeState.value.inProgress) {
|
||||
progressByTimeout = if (composeState.value.inProgress) {
|
||||
delay(500)
|
||||
composeState.value.inProgress
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
val showVoiceButton = !nextConnect && cs.message.text.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
|
||||
!composeState.value.forwarding && cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started) && (cs.contextItem !is ComposeContextItem.ReportedItem)
|
||||
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
|
||||
@@ -137,7 +128,7 @@ fun SendMsgView(
|
||||
}
|
||||
}
|
||||
when {
|
||||
progressByTimeout -> ProgressIndicator()
|
||||
cs.progressByTimeout -> ComposeProgressIndicator()
|
||||
cs.contextItem is ComposeContextItem.ReportedItem -> {
|
||||
SendMsgButton(painterResource(MR.images.ic_check_filled), sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendToConnect, sendMessage)
|
||||
}
|
||||
@@ -411,8 +402,8 @@ private fun RecordVoiceButton(interactionSource: MutableInteractionSource) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressIndicator() {
|
||||
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 3.dp)
|
||||
fun ComposeProgressIndicator() {
|
||||
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = MaterialTheme.colors.secondary, strokeWidth = 2.5.dp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+11
-1
@@ -695,9 +695,19 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque
|
||||
)
|
||||
}
|
||||
|
||||
fun acceptContactRequest(rhId: Long?, incognito: Boolean, contactRequestId: Long, isCurrentUser: Boolean, chatModel: ChatModel, close: ((chat: Chat) -> Unit)? = null ) {
|
||||
fun acceptContactRequest(
|
||||
rhId: Long?,
|
||||
incognito: Boolean,
|
||||
contactRequestId: Long,
|
||||
isCurrentUser: Boolean,
|
||||
chatModel: ChatModel,
|
||||
close: ((chat: Chat) -> Unit)? = null,
|
||||
inProgress: MutableState<Boolean>? = null
|
||||
) {
|
||||
withBGApi {
|
||||
inProgress?.value = true
|
||||
val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, contactRequestId)
|
||||
inProgress?.value = false
|
||||
if (contact != null && isCurrentUser) {
|
||||
val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf())
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
+61
-47
@@ -55,6 +55,7 @@ sealed class ActiveFilter {
|
||||
}
|
||||
|
||||
private fun showNewChatSheet(oneHandUI: State<Boolean>) {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
ModalManager.start.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
chatModel.newChatSheetVisible.value = true
|
||||
@@ -348,22 +349,64 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
|
||||
val updatingProgress = remember { chatModel.updatingProgress }.value
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
|
||||
if (oneHandUI.value) {
|
||||
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
|
||||
|
||||
if (appPlatform.isDesktop && oneHandUI.value) {
|
||||
val call = remember { chatModel.activeCall }
|
||||
if (call.value != null) {
|
||||
barButtons.add {
|
||||
val c = call.value
|
||||
if (c != null) {
|
||||
ActiveCallInteractiveArea(c)
|
||||
Spacer(Modifier.width(5.dp))
|
||||
}
|
||||
if (updatingProgress != null) {
|
||||
barButtons.add {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val hovered = interactionSource.collectIsHoveredAsState().value
|
||||
IconButton(onClick = {
|
||||
chatModel.updatingRequest?.close()
|
||||
}, Modifier.hoverable(interactionSource)) {
|
||||
if (hovered) {
|
||||
Icon(painterResource(MR.images.ic_close), null, tint = WarningOrange)
|
||||
} else if (updatingProgress == -1f) {
|
||||
CIFileViewScope.progressIndicator()
|
||||
} else {
|
||||
CIFileViewScope.progressCircle((updatingProgress * 100).toLong(), 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!stopped) {
|
||||
}
|
||||
|
||||
if (stopped) {
|
||||
barButtons.add {
|
||||
IconButton(onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||
generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app)
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_report_filled),
|
||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||
tint = Color.Red,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (connectProgressManager.showConnectProgress != null) {
|
||||
barButtons.add {
|
||||
Box(Modifier.padding(horizontal = DEFAULT_PADDING_HALF)) {
|
||||
CIFileViewScope.progressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oneHandUI.value) {
|
||||
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
|
||||
|
||||
if (appPlatform.isDesktop && oneHandUI.value) {
|
||||
val call = remember { chatModel.activeCall }
|
||||
if (call.value != null) {
|
||||
barButtons.add {
|
||||
val c = call.value
|
||||
if (c != null) {
|
||||
ActiveCallInteractiveArea(c)
|
||||
Spacer(Modifier.width(5.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
barButtons.add {
|
||||
IconButton(
|
||||
onClick = {
|
||||
@@ -388,38 +431,6 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
|
||||
}
|
||||
}
|
||||
|
||||
if (updatingProgress != null) {
|
||||
barButtons.add {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val hovered = interactionSource.collectIsHoveredAsState().value
|
||||
IconButton(onClick = {
|
||||
chatModel.updatingRequest?.close()
|
||||
}, Modifier.hoverable(interactionSource)) {
|
||||
if (hovered) {
|
||||
Icon(painterResource(MR.images.ic_close), null, tint = WarningOrange)
|
||||
} else if (updatingProgress == -1f) {
|
||||
CIFileViewScope.progressIndicator()
|
||||
} else {
|
||||
CIFileViewScope.progressCircle((updatingProgress * 100).toLong(), 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (stopped) {
|
||||
barButtons.add {
|
||||
IconButton(onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||
generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app)
|
||||
)
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_report_filled),
|
||||
generalGetString(MR.strings.chat_is_stopped_indication),
|
||||
tint = Color.Red,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val canScrollToZero = remember { derivedStateOf { listState.firstVisibleItemIndex != 0 || listState.firstVisibleItemScrollOffset != 0 } }
|
||||
@@ -658,8 +669,11 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
|
||||
if (it.isNotEmpty()) {
|
||||
// if some other text is pasted, enter search mode
|
||||
focusRequester.requestFocus()
|
||||
} else if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
} else {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
searchShowingSimplexLink.value = false
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
|
||||
+25
-1
@@ -3,6 +3,8 @@ package chat.simplex.common.views.newchat
|
||||
import SectionItemView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -25,6 +27,24 @@ suspend fun planAndConnect(
|
||||
cleanup: (() -> Unit)? = null,
|
||||
filterKnownContact: ((Contact) -> Unit)? = null,
|
||||
filterKnownGroup: ((GroupInfo) -> Unit)? = null,
|
||||
): CompletableDeferred<Boolean> {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
val inProgress = mutableStateOf(true)
|
||||
connectProgressManager.startConnectProgress(generalGetString(MR.strings.loading_profile)) {
|
||||
inProgress.value = false
|
||||
cleanup?.invoke()
|
||||
}
|
||||
return planAndConnectTask(rhId, shortOrFullLink, close, cleanup, filterKnownContact, filterKnownGroup, inProgress)
|
||||
}
|
||||
|
||||
private suspend fun planAndConnectTask(
|
||||
rhId: Long?,
|
||||
shortOrFullLink: String,
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)? = null,
|
||||
filterKnownContact: ((Contact) -> Unit)? = null,
|
||||
filterKnownGroup: ((GroupInfo) -> Unit)? = null,
|
||||
inProgress: MutableState<Boolean>
|
||||
): CompletableDeferred<Boolean> {
|
||||
val completable = CompletableDeferred<Boolean>()
|
||||
val close: (() -> Unit) = {
|
||||
@@ -36,7 +56,9 @@ suspend fun planAndConnect(
|
||||
cleanup?.invoke()
|
||||
completable.complete(!completable.isActive)
|
||||
}
|
||||
val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink)
|
||||
val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink, inProgress = inProgress)
|
||||
connectProgressManager.stopConnectProgress()
|
||||
if (!inProgress.value) { return completable }
|
||||
if (result != null) {
|
||||
val (connectionLink, connectionPlan) = result
|
||||
val link = strHasSingleSimplexLink(shortOrFullLink.trim())
|
||||
@@ -259,6 +281,8 @@ suspend fun planAndConnect(
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cleanup()
|
||||
}
|
||||
return completable
|
||||
}
|
||||
|
||||
+19
-2
@@ -29,6 +29,7 @@ import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.CIFileViewScope
|
||||
import chat.simplex.common.views.chat.topPaddingToContent
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.contacts.*
|
||||
@@ -40,6 +41,12 @@ import kotlinx.coroutines.flow.filter
|
||||
|
||||
@Composable
|
||||
fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
}
|
||||
}
|
||||
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
|
||||
Box {
|
||||
@@ -472,6 +479,13 @@ private fun ContactsSearchBar(
|
||||
) {
|
||||
searchText.value = searchText.value.copy(it)
|
||||
}
|
||||
|
||||
if (connectProgressManager.showConnectProgress != null) {
|
||||
Box(Modifier.padding(end = DEFAULT_PADDING_HALF)) {
|
||||
CIFileViewScope.progressIndicator(sizeMultiplier = 0.75f)
|
||||
}
|
||||
}
|
||||
|
||||
val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } }
|
||||
if (hasText.value) {
|
||||
val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() }
|
||||
@@ -522,8 +536,11 @@ private fun ContactsSearchBar(
|
||||
if (it.isNotEmpty()) {
|
||||
// if some other text is pasted, enter search mode
|
||||
focusRequester.requestFocus()
|
||||
} else if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
} else {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
if (listState.layoutInfo.totalItemsCount > 0) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
searchShowingSimplexLink.value = false
|
||||
searchChatFilteredBySimplexLink.value = null
|
||||
|
||||
+25
-2
@@ -13,6 +13,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -34,6 +35,7 @@ import chat.simplex.common.model.ChatController.appPrefs
|
||||
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.CIFileViewScope
|
||||
import chat.simplex.common.views.chat.topPaddingToContent
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
@@ -574,6 +576,12 @@ fun AddContactLearnMoreButton() {
|
||||
|
||||
@Composable
|
||||
private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState<Boolean>, pastedLink: MutableState<String>, close: () -> Unit) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
connectProgressManager.cancelConnectProgress()
|
||||
}
|
||||
}
|
||||
|
||||
SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) {
|
||||
PasteLinkView(rhId, pastedLink, showQRCodeScanner, close)
|
||||
}
|
||||
@@ -614,10 +622,25 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState<String>, showQRC
|
||||
)
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(MR.strings.tap_to_paste_link))
|
||||
Box(Modifier.weight(1f)) {
|
||||
Text(stringResource(MR.strings.tap_to_paste_link))
|
||||
}
|
||||
if (connectProgressManager.showConnectProgress != null) {
|
||||
CIFileViewScope.progressIndicator(sizeMultiplier = 0.6f)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LinkTextView(pastedLink.value, false)
|
||||
Row(
|
||||
Modifier.padding(end = DEFAULT_PADDING),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(Modifier.weight(1f)) {
|
||||
LinkTextView(pastedLink.value, false)
|
||||
}
|
||||
if (connectProgressManager.showConnectProgress != null) {
|
||||
CIFileViewScope.progressIndicator(sizeMultiplier = 0.6f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -862,6 +862,7 @@
|
||||
<string name="paste_the_link_you_received">Paste the link you received</string>
|
||||
<string name="the_text_you_pasted_is_not_a_link">The text you pasted is not a SimpleX link.</string>
|
||||
<string name="tap_to_paste_link">Tap to paste link</string>
|
||||
<string name="loading_profile">Loading profile…</string>
|
||||
|
||||
<string name="invalid_qr_code">Invalid QR code</string>
|
||||
<string name="code_you_scanned_is_not_simplex_link_qr_code">The code you scanned is not a SimpleX link QR code.</string>
|
||||
|
||||
Reference in new issue
Block a user